Menu hamburger icon

Style your app automatically with Onsen UI

Automatic Styling

As we mentioned in a previous post, we just released a new killer feature called automatic styling for Onsen UI 2.0. This feature allows you to style your app for both iOS and Android automatically with little to no effort.

In this post we will explain how to make a sample To-Do app step by step using as many Onsen UI 2.0 features as we can: implementation in Vanilla JavaScript (framework agnostic), automatic styling, notification promises and combination of Navigator + Tabbar + Splitter (new sliding menu) in the same app.

We will develop and test the app with Monaca since we can easily display the app as iOS or Android in the browser by just changing the device type in the preview settings. And most importantly, we can see (and debug) the final result in any real device with Monaca Debugger in no time.

Monaca auto styling

For local development, you can always use the device emulator included in Google Chrome’s DevTools and switch between iPhone and Nexus.

To-Do List app

We will make a simple To-Do List app. This app is perfect to show what we can do with the new beta in Vanilla JavaScript. First, you can play a bit with the app in the following iframes. You can also open this app link in your device to see the automatic styling in action.

As usual, the code is available on Github. It’s full of explanatory comments so it should be easy to follow!

As mentioned above, this sample app is implemented in Vanilla JavaScript without any other library or framework, so it could be a good reference when you start your own app. There are many ways to make a To-Do List and here we just show one of them. Perhaps not the best, perhaps not the worst, but we believe it could be instructive. Please choose your own style when you start your projects.

Anyhow, let’s get started with the tutorial.

App structure: HTML

We will put all the navigation components (ons-navigator, ons-splitter, ons-tabbar) in index.html using ons-template so we can easily manipulate the flow in the same file. The pages which this components load will be located in html directory as separate files.

What we want

First issue we need to think about is how we want to structure the app to fit our needs. Here, we want to have:

  • Task lists

A list of “pending tasks” and a list of “completed tasks” separated in a tabbar.

  • Categories

A list of categories that we can use to filter the tasks. We want this to be located in a menu to put it out of the normal view.

  • Create new tasks

A page where we can add new tasks. In this page we shouldn’t be able to see either category list nor task lists.

  • Modify existing tasks

A page where we can modify existing tasks and see their details. Same as the previous one, we don’t want to see categories or other tasks here.

What we use

Then, let’s choose the order of the components wisely so afterwards we don’t need to do extra job to make it work:

  • Tabbar

Starting from the lowest level navigation component, we want an ons-tabbar to separate our pending list page from our completed list page in two tabs.

<ons-template id="tabbar.html">
  <ons-page id="tabbarPage">
    ...

    <ons-tabbar id="myTabbar" position="auto">
      <ons-tab page="html/pending_tasks.html" label="Pending" active>
      </ons-tab>
      <ons-tab page="html/completed_tasks.html" label="Completed">
      </ons-tab>
    </ons-tabbar>
  </ons-page>
</ons-template>
  • Splitter (menu)

We need a menu common to these two lists. Therefore, ons-splitter component should be the parent of the previous ons-tabbar. Otherwise we would need to create two splitters, one for each list page.

<ons-template id="splitter.html">
  <ons-splitter id="mySplitter">
    <ons-splitter-side page="html/menu.html" swipeable width="250px" collapse swipe-target-width="60px">
    </ons-splitter-side>
    <ons-splitter-content page="tabbar.html">
    </ons-splitter-content>
  </ons-splitter>
</ons-template>
  • Navigator

Now that we have the basic functionality covered with the previous two components, we want to hide all of this and change the view to a “new task” or “modify task” page. We can achieve this by using an ons-navigator as the parent component of the other two. This navigator will change all its content when pushing a new page, hence hiding the menu and the tabs when we create/modify a task.

<ons-navigator id="myNavigator" page="splitter.html">
</ons-navigator>

Notice that this navigator is not wrapped inside ons-template since it’s the main component of the app.

App structure: JS

In this sample app we have structured our JS code in a specific way. We don’t want to mix our markup with our logic so we avoid to include onclick or onchange attributes in our elements. We will define some initialization function (controller) for every page and set up its functionality from there. This is called unobtrusive JavaScript. Also, we will store all our logic in a window.myApp object instead of using global variables for everything we need to store. This way we don’t pollute the global scope (except for myApp object itself). As a side note, we are using querySelector all along the app just to be homogeneous instead of using getElementById, getElementsByClass, etc. Thus, our app will be organised in the following way:

  • app.js

Contains the basic app setup: call the corresponding initialization controllers when the pages are ready and fills the task list with initial data. This is the important part:

window.myApp = {};

document.addEventListener('init', function(event) {
  var page = event.target;

  if (myApp.controllers.hasOwnProperty(page.id)) {
    myApp.controllers[page.id](page);
  }

  ...
});

We listen for page init events and trigger the corresponding page initialization function that we store in myApp.controllers.

  • controllers.js

As already mentioned, here we store all the initialization functions. For example, let’s have a look to “New Task Page Controller”:

myApp.controllers = {
  ...,

  newTaskPage: function(page) {

    page.querySelector('[component="button/save-task"]').onclick = function() {
      var newTitle = page.querySelector('#title-input').value;

      if (newTitle) {

        myApp.services.tasks.create(
          {
            title: newTitle,
            category: page.querySelector('#category-input').value,
            description: page.querySelector('#description-input').value,
            highlight: page.querySelector('#highlight-input').checked,
            urgent: page.querySelector('#urgent-input').checked
          }
        );

        ...

      } else {
        ons.notification.alert('You must provide a task title.');
      }
    };
  },

  ...
};

As you can see there, we just add some functionality to the “Save” button in New Task page. We have included component attribute just to be able to find these elements later on and add their functionality.

Specifically, here we create a new task item on click using the tasks service. In case the title input is left blank, we show an alert and abort the item creation.

  • services.js

Here comes the dense part. These are the functions that do the hard work in our app. We have organised them in “services” although behind the scenes this is just a JavaScript object with sub-objects:

myApp.services = {
  tasks: {
    create: function() {},
    update: function() {},
    ...
  },
  categories: {
    create: function() {},
    ...
  },
  animators: {
    ...
  },
  ...
};

For instance, let’s see how we can create a new task item when myApp.services.tasks.create(...) is called. This is the first part:

create: function(data) {
  // Task item template.
  var template = document.createElement('div');
  template.innerHTML =
    '<ons-list-item tappable category="' + data.category + '">' +
      '<label class="left">' +
       '<ons-input type="checkbox"></ons-icon>' +
      '</label>' +
      '<div class="center">' +
        data.title +
      '</div>' +
      '<div class="right">' +
        '<ons-icon style="color: grey" icon="' +
          (ons.platform.isAndroid() ? 'md-delete' : 'ion-ios-trash-outline') +
        '"></ons-icon>' +
      '</div>' +
    '</ons-list-item>'
  ;


  // Takes the actual task item.
  var taskItem = template.firstChild;

  // Store data within the element.
  taskItem.data = data;

  ...
}

Here we just create a new ons-list-item with the given data object that contains title, category name, description, etc. If we could just use ES2015 template literals for this task it would be much easier, wouldn’t it?

Since in Onsen UI 2.0 every component inherits from HTMLElement you could create them using document.createElement('ons-list-item'), for example. This would create a <ons-list-item></ons-list-item> node without any custom attribute and it would be compiled as is. However, in general we need to create elements directly with attributes and content so the component compilation actually consider this information. To do this we can create an empty div element (var template in our case) and insert our custom component inside with the innerHTML property. After this you just need to extract the content of this div element (template.firstChild for us).

Moreover, in this app we are storing the data of each task within the ons-list-item itself rather than in internal arrays (taskItem.data = data;). This is a very simple and handy approach possible in Vanilla JavaScript or jQuery, but in case you use a framework like AngularJS or React.js this could be troublesome. When these frameworks copy/move elements the inner properties could disappear, so it is advisable to store the data in real arrays and let the framework manage the rest.

In this list item template we set a category attribute that we will use later on to filter tasks depending on their categories. This way, we will be able to find the ones that don’t match our selected category with a simple query and set item.display = 'none' to those we want to hide. In case you don’t know what class="left" and the other classes mean, please read this previous post to understand how they work. We will go into details about the ons-icon that appears in this template in the following section. For now, let’s have a look at the next part of this service:

  // Add 'completion' functionality when the checkbox changes.
  taskItem.data.onCheckboxChange = function(event) {
    myApp.services.animators.swipe(taskItem, function() {
      var listId = (taskItem.parentElement.id === 'pending-list' && event.target.checked) ? '#completed-list' : '#pending-list';
      document.querySelector(listId).appendChild(taskItem);
    });
  };
  taskItem.addEventListener('change', taskItem.data.onCheckboxChange);

This adds some functionality to complete or undo the task. We are adding a listener for checkbox’s change event to handle this. Whenever the checkbox changes, the list item will notice and act consequently. We could just use onclick as well but for the sake of showing more possibilities we chosed this other approach. As you can see in this code, our completion functionality basically changes the item from “pending” list to “completed” list. This makes the app super simple, without the need of moving internal data in arrays. Once again, if you use complex frameworks you should leave to them the DOM manipulation. But this is just Vanilla :)

As for the animators service, don’t worry too much, it simply adds custom CSS classes with keyframes and timeouts and finally run the callback we pass as parameter when the animation finishes. Notice that these animations are not part of Onsen UI, we just made them for this sample app so we won’t stop here (you can check the Github repo instead).

  // Add button functionality to remove a task.
  taskItem.querySelector('.right').onclick = function() {
    myApp.services.tasks.remove(taskItem);
  };

  // Add functionality to push 'details_task.html' page with the current element as a parameter.
  taskItem.querySelector('.center').onclick = function() {
    document.querySelector('#myNavigator')
      .pushPage('html/details_task.html',
        {
          animation: 'lift',
          data: {
            element: taskItem
          }
        }
      );
  };

Remember that, since we added an event listener to the list item, we will need to remove it when we delete the element to prevent memory leaks. That’s why we saved the handler in a named property taskItem.data.onCheckboxChange, so we can do taskItem.removeEventListener('change', taskItem.data.onCheckboxChange) at any time. This is what myApp.services.tasks.remove(taskItem); exactly does, and we want it to happen when we click on the right part of the list item (where the trash icon is located).

Also, we want to push a new page when we click on the body of the task so we can see its information and modify it. In order to do this, we just take our navigator and perform a pushPage passing a reference to the item itself rather than an index. This simplifies the app avoiding the need for internal arrays, as already mentioned. That’s the reason why we stored taskItem.data within the element before. Again, in Angular or other framework you’d need to pass an index instead.

And finally, we append this item to the pending tasks list:

var pendingList = document.querySelector('#pending-list');
pendingList.insertBefore(taskItem, taskItem.data.urgent ? pendingList.firstChild : null);

If the second argument of insertBefore is null, the item is appended to the end of the list. This way we insert urgent tasks at the top and non urgent tasks at the bottom.

Automatic Styling

So far we have seen how to structure the app in an organised way, separating the view from the logic. Let’s see now how to make a good use of the automatic styling tools.

In general, the styling will be automatic by default unless you call ons.disableAutoStyling() at the beginning of your app or specify disable-auto-styling attribute in those elements you want to style manually. This means that most of your app will get Material Design styles automatically when running on Android. However, there will be parts in the app where this is not possible. For example, in order to save some new information in iOS we may have a large button at the bottom or a toolbar button at the top, while in Android we’d possibly have a floating action button instead. We thought it would be better to leave this decisions to the developer rather than assuming them by ourselves. However, we released a bunch of tools to make this task quite easy. Let’s have a look at different places of this app where we use these tools.

  • Tabbar position

We defined our tabbar element in index.html. We want it to be displayed at the bottom for iOS and at the top for Android. So far we could only specify position="top" or position="bottom", but from now on we can also use position="auto":

<ons-tabbar id="myTabbar" position="auto">
  ...
</ons-tabbar>

This will display the tabbar on top or bottom depending on the platform. The default behavior if you don’t specify any position is still position="bottom", so make sure you specify position="auto" if you want this behavior.

  • ons-if

There will be parts of the layout that only make sense on iOS and others only on Android. That’s why we created ons-if component. This element accepts an attribute platform with ios, android, windows and other as possible values. Apart from that, it also accepts orientation attribute so you can display elements when the device is in landscape or portrait mode. For example, in order to use different button elements to push pages in index.html, we do the following:

<ons-toolbar>
  ...
  <div class="right">
    <!-- Toolbar-button  only visible for iOS/other. -->
    <ons-if platform="ios other">
      <ons-toolbar-button component="button/new-task">New</ons-toolbar-button>
    </ons-if>
  </div>
</ons-toolbar>

<!-- Floating Action Button only visible for Android. -->
<ons-if platform="android">
  <ons-fab position="right bottom" component="button/new-task">
    <ons-icon icon="md-edit"></ons-icon>
  </ons-fab>
</ons-if>

...

Notice that both of them have the same component="button/new-task" attribute since they will do exactly the same. However, only one of them will be present at the same time since the other will be removed by ons-if.

Another example of ons-if can be seen in the details page:

<ons-list-item modifier="nodivider">
  <ons-if platform="ios other" class="left left-label">
    Title
  </ons-if>
  <div class="center">
    <ons-input id="title-input" type="text" placeholder="I want to..." float></ons-input>
  </div>
</ons-list-item>

These inputs are automatically filled with the task data. In Material Design it’s possible to see the floating label explaining what’s the content of the input but iOS does not display such information. Therefore, we want to display a static label on the left of the ons-list-item but we want this left part to be visible only on non Android platforms. To accomplish this we just use ons-if as the left part of the items with class="left". This way, Android won’t have a left part on its list items right as we want.

  • ons.platform

ons.platform contains a bunch of methods to check the current platform. Two of them are isAndroid() and isIOS(), which return a boolean value. For more details check out the reference page. As seen in the previous section, we can use this utilities to make decisions in JavaScript like choosing an icon or another for the template:

... +
'<ons-icon ... icon="' + (ons.platform.isAndroid() ? 'md-delete' : 'ion-ios-trash-outline') + '"></ons-icon>' +
...
  • Animations

The default animations for ons-navigator now changes depending on the platform. The default pushing page animation for iOS remains the same (slide) but for Android we have now some kind of fade + lift animation. Also the basic lift animation slightly differs in Android since it creates a black mask behind the pushed page. If you want to fix an animation for every platform you need to specify { animation: ... } as options in the navigator methods.

In this app we also change tabbar animation to be “slide” only on Android. To do this we set the animation attribute from the initialization controller:

document.querySelector('#myTabbar').setAttribute('animation', ons.platform.isAndroid() ? 'slide' : 'none');

Promises for async methods

Every asynchronous method now returns a promise that resolves to the elment that the method is pushing, popping, etc. In this app we use ons.notification to easily create dialogs and handle the user input with the new promises:

ons.notification.confirm(
  {
    title: 'Save changes?',
    message: 'Previous data will be overwritten.',
    buttonLabels: ['Discard', 'Save']
  }
).then(function(buttonIndex) {

  if (buttonIndex === 1) {
    // If 'Save' button was pressed, overwrite the task.
    myApp.services.tasks.update(element,
      {
        title: ...,
        category: ...,
        ...
      }
    );

    ...
  }
});

Conclusion

The automatic styling feature truly makes possible to write a hybrid app and run it on both iOS and Android (and perhaps Windows Phone at some point, why not). It will likely make your app development process much smoother. And that’s what we want here at Onsen UI, make your hard development easy, so we hope you like this feature and enjoy using it. In case you find any issue with this, please report it in the community forum (questions) or in our Github repo (bugs). And of course, leave comments if you have anything to add :)

If you liked what you’ve seen here, please don’t forget to leave us a star on GitHub. Happy coding!

Comments