Using Famo.us with Trigger.io

In this tutorial you are going to write a simple application based on the Famo.us UI framework and enhance it with several Forge native modules:

The full source code for this demo is available at: https://github.com/trigger-corp/tutorial-famous

Prerequisites

Before you can proceed with the main tutorial there are a couple of things you will need to do:

Install Nodejs

To install Nodejs you can either use your favourite package manager or download an installer from: http://nodejs.org/download/

Note: On newer versions of Ubuntu the packaged node binary has been renamed to nodejs due to a conflict with another package. Unfortunately this has the side-effect of breaking several Node.js tools. The workaround is to install the nodejs-legacy package: sudo apt-get install nodejs-legacy

Install Yeoman and the triggerio-famous generator

To simplify the initial setup of our Famo.us Trigger.io application you'll be using Yeoman to generate a seed app which is pre-configured with everything you need to write a Famo.us app.

You can install the Yeoman generator via npm with:

npm install -g grunt-cli yo bower generator-famous-triggerio

Generate a Famo.us Trigger.io seed app

With those prerequisites out of the way you're now ready to create a Famo.us Trigger.io app.

To generate the seed app you need to create a directory for your app and then invoke the triggerio-famous generator as follows:

mkdir tutorial
cd tutorial
yo famous-triggerio

The generator will now prompt you with a few self-explanatory questions and generate a seed app. Once it has completed you can build and run your app with:

forge build ios && forge serve ios

You should see:

A quick tour of the seed app files

The contents of your tutorial/ directory should look something like this:

tutorial/
├── bower.json
├── local_config.json
└── src/
    ├── config.json
    ├── css
       └── default.css
    ├── identity.json
    ├── index.html
    ├── js/
       ├── main.js
       ├── requireConfig.js
       └── views
           └── AppView.js
    └── lib/
        ├── famous/
        ├── famous-polyfills/
        └── requirejs/

Configure Forge native modules

Before you can use the Forge API's needed for this tutorial you'll have to enable the relevant modules to your app's configuration.

Open your app's src/config.json file in a text editor and change the "modules" section to look like this:

"modules": {
    "file": {
        "version": "2.4"
    },
    "request": {
        "version": "2.5",
        "config": {
            "permissions": []
        }
    },
    "topbar": {
        "version": "2.4",
        "disabled": false
    },
    "tabbar": {
        "version": "2.4",
        "disabled": false
    }
}

Now, when you rebuild and run your app it should display a top bar and a tab bar:

Configure your app to use Handlebars.js

In this tutorial we'll also be showing you how to use Handlebars templates to work with HTML/CSS resources.

While you could just download the Javascript library and include it in index.html manually this would be a good opportunity to show you how Famo.us uses Bower and RequireJS to manage your app's dependencies.

If you've already installed the Famo.us Trigger.io generator above you should have everything you need.

To start, open up the bower.json file in your app's root directory and scroll down to the dependencies section:

"dependencies": {
    "requirejs": "~2.1.11",
    "famous-polyfills": "git+https://github.com/Famous/polyfills.git#0.1.1",
    "famous": "~0.2.2"
}

This section works much like Node's package.json file and specifies the app dependencies you'd like bower to manage for you.

To add Handlebars support to your app simply edit the section as follows:

"dependencies": {
    "requirejs": "~2.1.11",
    "handlebars": "~1.3.0", // <-- Here
    "famous-polyfills": "git+https://github.com/Famous/polyfills.git#0.1.1",
    "famous": "~0.2.2"
}

Once you've done this you can run the following command and bower will take care of downloading the Handlebars library and placing it in the src/lib directory of your app:

bower install

Once the Handlebars library is in place the last thing you have to do is to let RequireJS know about it so that it's available to your app.

To do this, open src/js/requireConfig.js and edit it as follows:

require.config({
    paths: {
        "famous": "../lib/famous",
        "requirejs": "../lib/requirejs/require",
        "handlebars": "../lib/handlebars/handlebar" // <-- Here
    }
});
require(["handlebars", "main"]); // <-- Here

That's it!

TODO Download assets for the demo

Finally, to save you some typing I've put together an archive of some core files required by this tutorial that you can download here

If you unpack this archive in your app's directory it should create the following files:

 src/css/default.css
 src/js/Templates.js // ?
 src/js/views/ListView.js
 src/js/views/NativeView.js // ?
 src/js/views/SettingsView.js
 src/img/camera.png
 src/img/forge-logo.png
 src/img/hamburger.png
 src/img/refresh.png

Part I - The main application view

If you've ever tried to implement a scrolling list view in a hybrid app you'll know just how hard it is to get it working smoothly! Therefore, to demonstrate the power of using Famo.us with Trigger.io, you're going to build a simple timeline viewer that displays images in a scrolling list view.

The implementation is reasonably straightforward:

  1. Create a new Famo.us view in src/js/views/ListView.js that encapsulates the functionality of your scrolling list view.
  2. Add your ListView it to the main app view in src/js/views/AppView.js
  3. Create a method that queries a remote API endpoint for images and populate the list.

1. Create a scrolling list view

To save time I won't be going into very much detail about the inner workings of this view.

Esentially it's a Famo.us View which contains a Famo.us ScrollView and exposes one public method for adding items to the scroll view:

ListView.prototype.addItem = function (content_front, content_rear)

Grab a copy of ListView.js from here and copy it to js/views/ListView.js

2. Add the list view to the main app view

Open up src/js/views/AppView.js in an editor and take a look at the default AppView constructor created for the seed app:

function AppView() {
    View.apply(this, arguments);

    _createDefaultView.call(this);

    forge.logging.info("Add Javascript to src/js/views/AppView.js!");
}

To add your list view you're going to include the ListView module you just created and replace the call to _createDefaultView with your own function like so:

var ListView = require("views/ListView");

function AppView() {
    View.apply(this, arguments);

    _createListView.call(this);
}

Then add the following implementation to create the list view:

function _createListView() {
    this.listContainer = new ContainerSurface({
        properties: {
            backgroundColor: "#f4f4f4",
            boxShadow: "0 0 20px rgba(0, 0, 0, 0.5)"
        }
    });
    this.listModifier = new Modifier({
        transform: Transform.translate(0, 0, 0.1)
    });
    this.listView = new ListView();
    this.listContainer.add(this.listView);
    this.add(this.listModifier).add(this.listContainer);
}

To understand what's going on here I'll need to quickly explain some basic Famo.us concepts:

  • Surfaces: Surfaces are nodes that get drawn to the screen. ContainerSurface is a type of Surface designed to also contain other surfaces and set properties to be applied to all of them at once. Here we create a ContainerSurface that sets the background color and holds your list view.
  • Modifiers: A Modifier is a node that can modify nodes that are below it in the render tree. Here we create a modifier that will shift the list view container (and its content) to be slightly in front of any other views in the AppView.
  • Views: Views are nodes that reduce the boiler plate when creating a component. They provide a standard interface for adding to the Render Tree, handle events and manage state variables. ListView is implemented as a View.

Note: For more information you can see the Famo.us Render Tree Guide.

3. Create a method that queries a remote API endpoint for images and populate the list.

We'd like to add support later for querying the remote API endpoint for specific tags, so you'll need to add a state variable to AppView that will remember the currently selected tag. Look for the AppView.DEFAULT_OPTIONS declaration and edit it as follows:

AppView.DEFAULT_OPTIONS = {
    currentTag: "Popular"
};

Now that we have a current tag, let's write a function that will use the forge.request.ajax method to query the remote API end point and populate our list view with items:

function _refreshListView() {
    var client_id = "e8f3e3e90a0d466484df7fac556c51da";
    var tag = this.options.currentTag.toLowerCase();
    var url;
    if (tag === "popular") {
        url = "https://api.instagram.com/v1/media/popular";
    } else {
        url = "https://api.instagram.com/v1/tags/" + tag + "/media/recent";
    }
    forge.request.ajax({
        url: url + "?client_id=" + client_id,
        dataType: "json"
    }, function (response) {
        response.data.forEach(function (item) {
            this.listView.addItem(
                "<img src='" + item.images.low_resolution.url + "' />",
                "TODO in the next step"
            );
        }.bind(this));
    }.bind(this), function (error) {
        console.error("Request failed: " + JSON.stringify(error));
    });
}

Finally, add a call to your _refreshListView method in the AppView constructor:

function AppView() {
    View.apply(this, arguments);

    _createListView.call(this);
    _refreshListView.call(this);
}

If you rebuild your app you should now see a scrolling list of images that flip over to reveal a rear view when touched.

All at a silky-smooth 60fps!

Part II - Use Handlebars and CSS to style your views

One of the biggest differences between Famo.us and more traditional web frameworks is that the bulk of your app layout is defined in your Javascript code rather than HTML and CSS.

This is not to say that HTML/CSS no longer has a role to play but rather that you will more frequently finding yourself using it to style the content of individual Famo.us Surfaces rather than using it to layout your user interface elements on the page.

If we look at the app we created above it could certainly do with a bit of styling to make it look nice so let's create two templates, one for the front of the list item and one for the rear.

Open up your app's src/index.html file and add the following code between the <body> tags:

<script id="template-front" type="text/x-handlebars-template">
    <div class="front">
        <img class="image" src="{{images.low_resolution.url}}" />
    </div>
</script>

<script id="template-rear" type="text/x-handlebars-template">
    <div class="rear">
        <img class="icon" src="{{user.profile_picture}}" />
        <div class="user">{{user.full_name}}</div>
        <div class="caption">{{pretty caption.text}}</div>
        <div class="comments">
            {{#slice 0 5 comments.data}}
            <div class="comment">
                <b>{{from.full_name}}:</b> {{pretty text}}
            </div>
            {{/slice}}
        </div>
    </div>
</script>

This will define two Handlebars templates that take an item returned from the API endpoint we created above as arguments.

Before you can use these templates in your code you'll need to ensure that they get compiled at app startup. To do this we'll create a module we can load with RequireJS.

Open the file src/js/Templates.js and add the following code to the end:

    ...

    // Template helper to slice a list
    Handlebars.registerHelper("slice", function (from, to, context, options) {
        ...
    });

    // Compile templates <-- here
    module.exports.front = Handlebars.compile(document.getElementById("template-front").innerHTML);
    module.exports.rear = Handlebars.compile(document.getElementById("template-rear").innerHTML);
});

Now, open src/js/AppView.js and add the following line to your dependencies:

var Templates = require("Templates");

Finally, scroll to the _refreshListView() function and modify the call to this.listView.addItem() as follows:

this.listView.addItem(Templates.front(item),
                      Templates.rear(item));

Now, whenever an item is added to the list view it will first be passed through the templates we defined in src/index.html to produce a much better looking UI:

Part III - Create a view for your Native UI elements

As mentioned earlier, Famo.us has a concept of "Views" that act as containers for UI elements and event handling.

While you don't have to use a Famo.us View to manage the Native UI elements provided by Forge it can help greatly in organizing your code.

We're not going to write any code for this part but I'll step you through one way you can organize Trigger.io native UI elements with Famo.us:

If you look inside src/js/NativeView.js you'll see a similar structure to src/js/AppView.js where we first include any dependencies used by the view:

define(function(require, exports, module) {
    var View = require("famous/core/View");

Followed by a view constructor responsible for creating and setting up the basic view configuration:

function NativeView() {
    View.apply(this, arguments);

    _createTabBar.call(this);
    _showTopBar.call(this);
    _createEvents.call(this);
}
NativeView.prototype = Object.create(View.prototype);
NativeView.prototype.constructor = NativeView;

Next we define any state variables used by the view:

NativeView.DEFAULT_OPTIONS = {
    currentTag: "Popular"
};

Finally, we have the methods used by the constructor. Let's step through _createTabBar() function quickly:

function _createTabBar() {
    forge.tabbar.addButton({
        icon: "/img/camera.png"
    }, function (button) {
        button.onPressed.addListener(function () {
            this._eventOutput.emit("clickCamera");
        }.bind(this));
    }.bind(this));
}

If you've worked with Trigger.io Forge before you'll recognize the calls to forge.tabbar.addButton() and button.onPressed.addListener which adds a button to the tab bar and binds a callback function to be invoked when the button is clicked.

What's new however is this:

this._eventOutput.emit("clickCamera");

It's really pretty cool, by deriving our NativeView from the Famo.us View component we get to use the Famo.us event system!

Every Famo.us View comes with two EventHandler instances: _eventOutput and _eventInput which allow us to both emit events to other Famo.us views as well as receive events from Famo.us views.

Moar: To learn more about Famo.us event-handling see the Famo.us Event Guide

You now have all the pieces needed to understand the code:

  1. Add a button to the Forge tabbar.
  2. Listen for a button pressed event.
  3. When a button press event occurs, emit the event from NativeView as a Famo.us event.

Note: If you're not familiar with the function () { ... }.bind(this) syntax used above please don't sweat, it's quite simple! By calling the bind method on a callback function we can specify what this will refer to inside the callback when it is invoked. An alternate syntax would be to write something like:

var thisModule = this;
forge.tabbar.addButton({
    icon: "/img/camera.png"
}, function (button) {
    button.onPressed.addListener(function () {
        thisModule._eventOutput.emit("clickCamera");
    });
});

Part IV - Subscribing To Events

Now that you understand how to use a Famo.us view with Forge native components we need to wire up our NativeView with the main AppView.

Open up src/js/AppView.js, import NativeView and modify the view constructor as follows:

var NativeView = require("views/NativeView");  // <-- Here

function AppView() {
    View.apply(this, arguments);

    _createNativeView.call(this);   // <-- Here
    _createListView.call(this);
}

Add a function definition for _createNativeView like so:

function _createNativeView() {
    this.nativeView = new NativeView();
    this.subscribe(this.nativeView);
}

Take note of the call to this.subscribe(this.nativeView) - what this does is to subscribe the AppView component to receive any events generated by your NativeView.

Part V - Add support for the device camera

Open up src/js/AppView.js and add the following function to the AppView component:

function _takePhotograph() {
    forge.file.getImage({
        width: 290,
        source: "camera",
        saveLocation: "file"
    }, function (file) {
        forge.file.URL(file, function (url) {
            var item = {
                user:    { profile_picture: "img/forge-logo.png",
                           full_name: "Forge" },
                images:  { low_resolution: { url: url } },
                caption: { text: "Made with @befamous and @triggercorp" },
                comments: { data: [
                    { from: { full_name: "antoinevg"  }, text: "feeling excited about #hybrid apps yet?" }
                ] }
            };
            this.listView.addItem(Templates.front(item),
                                  Templates.rear(item));
        }.bind(this));
    }.bind(this));
}

You're making a call to forge.file.getImage with the source set to camera and then, in the callback, you create an item with the same object structure as our API endpoint before adding it to the ListView

Now we can add an event handler for the clickCamera event. Open up src/js/AppView.js and make the following changes to the view constructor:

function AppView() {
    View.apply(this, arguments);

    _createNativeView.call(this); 
    _createListView.call(this);

    _createEvents.call(this);       // <-- Here
}

Because AppView is subscribed to events from NativeView and will receive the clickCamera event when the camera icon is pressed we can now subscribe to the event.

To handle subscribing to the event the implementation of _createEvents() looks like this:

function _createEvents() {
    this._eventInput.on("clickCamera", function () {
        _takePhotograph.call(this);
    }.bind(this));
}

Now, when you build and run your app you should be able to press the camera icon to take a photo which will then appear in your ListView

Part VI - Create a sliding menu

The last thing I'm going to show you now is how to create a sliding menu for choosing the tags to display in the timeline and wire up the buttons in the forge.topbar to control it.

The implementation of our settings menu component lives in src/js/views/SettingsView.js and is responsible for:

  • Creating a Famo.us SequentialLayout that will hold the menu items.
  • Adding a bunch of tag names to the menu.
  • Emitting a clickTag event after selecting a menu item.

To add it to AppView open up src/js/views/AppView.js and create a function as follows:

function _createSettingsView() {
    this.settingsContainer = new ContainerSurface({
        properties: {
            overflow: "hidden",
            backgroundColor: "white"
        }
    });
    this.settingsView = new SettingsView();
    this.subscribe(this.settingsView);
    this.settingsContainer.add(this.settingsView);
    this.add(this.settingsContainer);
}

Note the call to this.subcribe(this.settingsView) - this will ensure that AppView receives the clickTag event emitted by the menu.

Add _createSettingsView() to the AppView constructor:

function AppView() {
    View.apply(this, arguments);

    _createNativeView.call(this);
    _createSettingsView.call(this);  // <-- Here
    _createListView.call(this);

    _createEvents.call(this);

    this.trigger("clickRefresh");
}

Now, when you run the app it will create the settings menu and add it to your main view but it won't be visible until we add some logic to create the sliding animation.

If you take a look at the _showTopBar() function in src/js/views/NativeView.js you'll see the code that creates the "hamburger" button on the topbar:

forge.topbar.addButton({
    icon: "/img/hamburger.png",
    position: "left"
}, function () {
    _showTopBarBurger.call(this);
    this._eventOutput.emit("clickBurger");
}.bind(this));

In the callback we're doing two things, first we call _showTopBarBurger() which replaces the topbar buttons with a "Done" button and then we're getting the NativeView component to emit a clickBurger event.

Now, go back to src/js/views/AppView.js and modify the _createEvents() function as follows:

function _createEvents() {
    this._eventInput.on("clickBurger", function () {
        _openBurgerMenu.call(this);
    }.bind(this));
    this._eventInput.on("clickDone", function () {
        _closeBurgerMenu.call(this);
    }.bind(this));
    this._eventInput.on("clickRefresh", function () {
        _refreshListView.call(this);
    }.bind(this));
    this._eventInput.on("clickCamera", function () {
        _takePhotograph.call(this);
    }.bind(this));
    this._eventInput.on("clickTag", function (tag) {
        this.options.currentTag = tag;
        _refreshListView.call(this);
        this.nativeView.trigger("clickDone", tag);
    }.bind(this));
}

You've now added a number of new event handlers:

  • clickBurger: Handle the event emitted when pressing the "burger" icon on the topbar
  • clickTag: Handle the event emitted when selecting a tag in the sliding menu
  • clickDone: Handle the event emitted when pressing the "Done" button on the topbar

The only thing left to do before our sliding menu is fully operational is to implement the _toggleMainView() function responsible for sliding it in and out.

Luckily the implementation is really simple:

function _openBurgerMenu() {
    this.listModifier.setTransform(Transform.translate(276, 0, 1.0), {
        duration: 400,
        curve: "easeInOut"
    });
}

function _closeBurgerMenu() {
    this.listModifier.setTransform(Transform.translate(0, 0, 1.0), {
        duration: 400,
        curve: "easeInOut"
    });
}

When we want to open the menu we simply change the transform on our ListView modifier to smoothly slide it 276 pixels to the right over 400ms.

To close the menu we simply slide it back to it's origin.

Final words

bloop