Tabris.js: Declarative UI in 100 lines of functional programming
Hi, as this is my first post on the EclipseSource Blog, I thought I would introduce myself. I’m Shai Alon - new employee at EclipseSource and the mobile developer evangelist for the Tabris.js technology. I am a full stack JavaScript expert and my previous job was a team leader of a full stack JavaScript team - building complex web applications with an Ember.js frontend and Node.js microservice backend. When I saw Tabris.js I was amazed by the technology and the potential of how it can disrupt mobile app development as we know it, and I knew I had to join the team.
Besides software development, I also love football (soccer), photography, and my recently born daughter. But enough about that, let’s talk about building a declarative UI for Tabris.js!
Intro
Tabris.js is an amazing way to create native mobile apps in the new age. It combines native platform widgets - performance plus look & feel, and device capabilities with the simplicity, elegance, and power of a single JavaScript codebase for iOS, Android and Windows (coming soon). These advantages make a very strong use case for JavaScript runtime platforms like Tabris.js, React Native and NativeScript, rather than HTML-based hybrid platforms like Ionic. We will certainly be seeing more and more apps built in JavaScript without HTML in the years to come.
Moreover, Tabris.js also offers some unique advantages to developers, like a cloud build service and developer apps for iOS and Android. This means you can develop iOS apps without owning a Mac or installing Xcode (but need an iPhone / iPad). Likewise, you can develop Android apps without downloading Android studio and the Android SDKs (but need an Android phone / tablet).
That said, as an evolving technology, there is still a lot to improve on. The beauty of it is that it’s open source so anyone can fork the code base, hack on it, and even become a contributor.
As I was building my first app with the platform I couldn’t help notice how the API is laser focused in its precision and ability to “control” the UI. However, I felt like I would be happy to trade some of this precision for more expressiveness and a more declarative code style that I was used to from web app development. That sent me on a journey of exploration - seeking to find a way to enjoy the advantages Tabris.js has to offer, while writing less (and cleaner) code.
The journey begins
I took the bookstore example from the Tabris.js repository, and I will show my thought process on a small snippet that is used to render the “Book details” section:
function createDetailsView(book) {
var composite = tabris.create("Composite", {
background: "white",
highlightOnTouch: true
});
tabris.create("Composite", {
layoutData: {left: 0, right: 0, top: 0, height: 160 + 2 * PAGE_MARGIN}
}).on("tap", function() {
createReadBookPage(book).open();
}).appendTo(composite);
var coverView = tabris.create("ImageView", {
layoutData: {height: 160, width: 106, left: PAGE_MARGIN, top: PAGE_MARGIN},
image: book.image
}).appendTo(composite);
var titleTextView = tabris.create("TextView", {
markupEnabled: true,
text: "" + book.title + "",
layoutData: {left: [coverView, PAGE_MARGIN], top: PAGE_MARGIN, right: PAGE_MARGIN}
}).appendTo(composite);
var authorTextView = tabris.create("TextView", {
layoutData: {left: [coverView, PAGE_MARGIN], top: [titleTextView, PAGE_MARGIN]},
text: book.author
}).appendTo(composite);
tabris.create("TextView", {
layoutData: {left: [coverView, PAGE_MARGIN], top: [authorTextView, PAGE_MARGIN]},
textColor: "rgb(102, 153, 0)",
text: "EUR 12,95"
}).appendTo(composite);
return composite;
}
The things I wanted to improve:
- Avoid writing
appendTo
for each of the elements (5 times) - Avoid writing
tabris.create
for every element I create (6 times) - Avoid saving references to items
var coverView = tabris.create(...)
- Extract layout data from the content
- Understand what the code does with a glance
As I started comparing the Tabris.js template architecture to that of the web and other JavaScript mobile technologies, I noticed that being an imperative “just JavaScript” technology also has various advantages over other UI declarative technologies, like HTML (+CSS) or Handlebars:
- It doesn’t have a “detached” part of the application like external HTML / template files.
- It has really great tooling with a good IDE (like WebStorm or Visual Studio Code), and can even be joined with TypeScript. Compare that to a
<div class=”.something”></div>
from the web where it’s sometimes hard to figure out which css styling will be applied to which element. - Event handlers like the
.on(“tap”, SOME_FUNCTION )
blend in perfectly in the JS world. - Since JavaScript code can be used in functions and required modules, it’s very easy to reuse code and “create your own components” - specific to your app.
I was on my way to find a better paradigm for Tabris.js - one that would fix the inconveniences, but still keep all the benefits over HTML and other template engines.
Functional programming to the rescue
I was reading an interesting post by André Staltz (functional programming expert). Something that really caught my eye was this well phrased explanation of what I was trying to accomplish:
“You want to choose the library/framework with the most ergonomic paradigm. That means a good signal-to-noise ratio: each line of code contributes to delivering features, each line of code is “semantic” and reads like a specification. In short, the closer the code is to a description of “what I want the program to do”, the better. But the more manual wiring there is, the more “noise” is added to your code, and the more verbose it gets.”
The article also explains how hyperscript-helpers can be used as a cleaner alternative to JSX with Cycle.js.
Inspired by this notion, I decided to apply this behaviour to the Tabris.js bookstore example. After a bit of tinkering using functional programming concepts, I wrote a small template engine that could render this:
function BookDetails(book) {
return (
Composite( bookDetailsStyle.container,[
Image( book.image, bookDetailsStyle.image),
Composite( bookDetailsStyle.textContainer , [
Text( book.title , bookDetailsStyle.title),
Text( book.author, bookDetailsStyle.author),
Text( book.price , bookDetailsStyle.price)
])
]).on("tap", () => { openReadBookPage(book) })
)
}
* I also added ES6 into the mix to get arrow functions and other syntactic goodies.
All the styling, layout data and config were moved to an external object:
const bookDetailsStyle = {
container: {
background: "white",
highlightOnTouch: true,
top: 0, height: 192, left: 0, right: 0
},
image: {
height: 160, width: 106, left: PAGE_MARGIN, top: PAGE_MARGIN
},
textContainer: {
left: ["prev()", PAGE_MARGIN],
top: PAGE_MARGIN,
right: PAGE_MARGIN
},
title: {
font: "bold 20px",
left: 0, top: "prev()", right: 0
},
author: {
left: 0, top: "prev()", right: 0
},
price: {
left: 0, top: ["prev()",PAGE_MARGIN],
textColor: "rgb(102, 153, 0)"
}
}
* Notice the use of the advanced layouts in Tabris.js using "prev ()"
.
A quick check shows everything that needed improvement was indeed improved:
- The
AppendTo
’s were replaced with a semantic structure of arrays passed to components. - The
tabris.create(“TextView”)
commands were replaced with a leanerText( content )
- There is no longer a need to save reference to elements.
- The layout data is extracted to an external source and referenced directly.
- It is easy to understand what the code does with a quick glance.
- All the advantages of the JavaScript approach still apply.
Will it scale?
Applications tend to grow in features and thus in the codebase demands, and we need to be sure this approach can scale well in the long term - I checked whether components, mixins and other paradigms I knew from web app development would blend in well.
It seems creating components is a natural behaviour with this technique. Using the BookDetails
function as a component in another template was very easy. Just pass it the book to render and it’s good to go:
function openBookPage(book) {
return (
Page ( book.title , [
BookDetails(book),
Spacer(),
BookTabs(book),
]).open()
)
}
It can also be moved to an external file and then required when needed. Adding mixins for animations also turned out to be very clean and simple:
Text( book.title , bookDetailsStyle.title , fadeInRight(500, 100) ),
Text( book.author, bookDetailsStyle.author , fadeInRight(500, 300) ),
Text( book.price , bookDetailsStyle.price , fadeInRight(500, 500) )
Also tooling is very powerful with this approach - the IDE can easily show the parameters and defaults for component / mixin:
What now?
In this experiment, all this goodness was added as external layer on top of Tabris.js that is just 100 lines of (lightweight) code in functional style in this file.
We are working to include a declarative UI, reusable component structure, animation mixins and much more into Tabris.js right now. In the meantime you can play around with my code and hack / comment on it on Tabris.js experiment playground on GitHub.
Please note that Tabris.js is not a framework that tries to do everything - it focuses on the core abilities to use native mobile platform UI widgets in a Cordova app, and offer a great cloud build service.
Hence, it doesn’t enforce any specific pattern, so that it can be easily combined with other libraries / frameworks or used as a powerful and lightweight standalone.
For more advanced use cases of data flow we would love to see community generated examples of Tabris.js integrated with frameworks that handle complex flows like Angular2 and Cycle.js.
Who knows, do that and you could spark the “angular-native” project the community has been craving for :)
Go ahead — get started by signing up at Tabris.js and doing the getting started examples — you will be creating your shiny new app in no time! Comment here and tell us what you would like us to add to the new template engine (or just fork the project and add it yourself).