Ractive.js tutorial - Creating the barebones (2 of 6)

(Previous step: Setting up your environment)

Ok, so now that you have all ready to fly, let's check what we want to do.

The goal is to build a simple app that let us view GitHub user's information and, also, to manage our own message notes for every user.
We will have a common layout with a static section (the one where you can search for users) and two subviews, having each one a different URL:

  • Home page: will only contain a welcome message
  • User profile page: will contain three sections for every user info group

This is the home page:
Home page

This is the user page:
User profile page

Our main layout will host the header and a dynamic section we will fill with the subsections depending on the URL and the user actions.

So, the first thing is to create our main html layout.
Open the index.html file and populate it with this content:

<!DOCTYPE html>  
<html>  
    <head>
        <meta charset="UTF-8">
        <title>Ractive Github notetaker</title>
        <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.css">
    </head>

    <body>
        <div id="app"></div>

        <script src="dist/js/app-bundle.js"></script>
    </body>
</html>  

This basic html just loads bootstrap for styling, creates an empty div where we will load our application content, and requests the JS bundle which will contains all of our magic code.

The way Ractive works it that you create a new instance of its main object specifying the html/handlebars template it will need to deal with, the place in the document it will fills, and the data it must manage (and display). Of course, there are a lot of other properties you can initialize instances with, but keep this idea by now.

Let's update our main JS file with a Ractive instance that will represent our application.
root-folder/app/js/app.js:

import Ractive from 'ractive';

let App = new Ractive({  
  el: '#app',
  template: '<input type="text" value="{{name}}"><p>Name: {{name}}</p>',
  data: {
    name: 'Paquitosoft'
  }
});

export default App;  

Before we go any further, we must install our first production dependency: Ractive.js

$ npm install ractive --save

Now, if you re/start your server (npm start), you will see a blank page with an input and a label with a text.
Note how changing the value from the input, the text besides de label gets updated.
This is because Ractive data binding, implemented just by linking the value of the input to the name attribute of the Ractive instance data.

If you've ever used Mustache or Handlebars before, Ractive template system is very similar. It's an extension of Mustache with some sugar to make your life easier.

Notice the template attribute we're passing in the Ractive instance initialization. It holds the contents of the template for the purpose of this little example, but we want to have that template in its own file for better organization.

Let's create a new folder for holding our templates and create the main application one.
root-folder/app/js/views/app.html

<div class="main-container">  
    <nav class="navbar navbar-default" role="navigation">
        <div class="col-sm-1">
            <a href="/">
                <h3>Notetaker</h3>
            </a>
        </div>
        <div class="col-sm-7 col-sm-offset-1" style="margin-top: 15px;">
            Here will be the search controls...
        </div>
    </nav>
    <div class="container">
        Here will be the main content of every route...
    </div>
</div>  

In order to use this file in our application main file, we need to import it. But, as by default webpack interprets all dependencies as JS, we need to configure it to understand our templates are only text and it doesn't need to process them.
We do this by configuring a new webpack loader which allows to simple import the files contents as they are (raw-plugin).
root-folder/webpack.config.js

module.exports = {  
    entry: './app/js/app.js',
    output: {
        filename: './dist/js/app-bundle.js'
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                loader: 'babel'
            },
            {
                test: /\.html$/,
                loader: 'raw'
            }
        ]
    }
};

Now we import the template in the app file, re/start our main process (npm start) and check http://localhost:8080.
root-folder/app/js/app.js:

import Ractive from 'ractive';  
import template from './views/app.html';

let App = new Ractive({  
  el: '#app',
  template: template,
  data: {
    name: 'Paquitosoft'
  }
});

export default App;  


Ractive components

On top of direct instances, Ractive allows us to use what they call Components.
The idea behind this kind of objects is to allow you to create encapsulated components very much like React does.
Once you declare and register them (can be globally or per instance/component), you can use it inside your templates as new html entities.

To demonstrate how they work, we will create a new component to encapsulate the search functionality.
We begin by creating the template in a new file:
root-folder/app/views/search-user.html

<div class="col-sm-12">  
    <form on-submit="searchUser">
        <div class="form-group col-sm-7">
            <input type="text" class="form-control" value="{{query}}" autofocus placeholder="{{placeholder}}"/>
        </div>

        <div class="form-group col-sm-5">
            <button type="submit" class="btn btn-block btn-primary">Search Github</button>
        </div>
    </form>
</div>  

Let's comment this template a little bit...

The on-submit attribute in the form tag is a Ractive proxy event declaration. Every time the user submits the form, the component managing this template will be notified (we'll see how to listen to that event in a moment).

The input tag has a data binding value with the query component data attribute. Ractive will keep the sync. The same goes for the placeholder.
While the first one is only used inside the component, we will provide the latter from the parent App instance just to show how you can share data between them.

You must know that components, by defuault, inherit its parent data context. This search component will be used from the App main instance, meaning that App's data attributes are, by default, visible to the search component.
Personally I don't like this kind of behavior by default as components get less reusable when they depend on some context not declared. I prefer to use them in isolated model, where they create an own data attributes context which you can pass specific parameters.

Let's create our component in a new file:
root-folder/app/js/components/layout/search-user.js

import Ractive from 'ractive';  
import Template from '../../../views/layout/search-user.html';

var SearchGithub = Ractive.extend({  
    isolated: true,
    template: Template,

    oninit() {
        this.on('searchUser', (rEvent) => {
            rEvent.original.preventDefault();

            let username = rEvent.context.query;

            console.log('This is the user you want to look up:', username);
        });
    },

    data: {
        query: ''
    }
});

export default SearchGithub;  

The way we create a Ractive component is by executing its extend function, passing the attributes which configure the component:

  • isolated: We set this attribute to false so a new independent data context it's created for this component.
  • temaplate: Set the template we previously declared.
  • oninit: This function will be executed everytime the component gets initialized (Check this link to review the info of all life-cycle events).
  • data: Here we define the inner properties.

Also, in our oninit handler we set up a listener for the on-submit event by listening to the custom event searchUser.
The handler receives an extension of the native browser event enhanced with some useful information (check the Event arguments section of the proxy events docs to see the details).
We access the original property where we find the native browser event just to prevent default behavior. Note that you could also get this by returning false from this listener function, but that also stops event propagation.
The context attribute for the event holds a data reference to the context where the event took place. In this case is the input tag, represented by an object with its data bindings.

So now that we have our component set up, let's use it from the App.
First we need to update the App markup:
root-folder/app/views/app.html

<div class="main-container">  
    <nav class="navbar navbar-default" role="navigation">
        <div class="col-sm-1">
            <a href="/">
                <h3>Notetaker</h3>
            </a>
        </div>
        <div class="col-sm-7 col-sm-offset-1" style="margin-top: 15px;">
            <SearchUser placeholder="Type a GitHub username..." />
        </div>
    </nav>
    <div class="container">
        Here will be the main content of every route...
    </div>
</div>  

Take note about how we're providing some data to the component by providing a tag attribute (placeholder).

Second, we update our App instance to tell it to use the search-user component:
root-folder/app/js/app.js

import Ractive from 'ractive';  
import template from '../views/app.html';  
import SearchUserComponent from './components/layout/search-user';

let App = new Ractive({  
  el: '#app',
  template: template,
  components: {
      SearchUser: SearchUserComponent
  }
});

export default App;  

We've just imported the search-user component and we set it as an App component using its components attributes. You just need to configure an object with the components you will use in your instance (or component) where the key is the name you will use for the component in the template and the value will be the component class.

You can also register the components globally like this:

Ractive.components.SearchUser = SearchUserComponent  

The benefit is that all Ractive instances and components will see the component.
The downside is that you loose visibility about which components your instance/component is using in the code.
This is just a personal choice, and I prefer being specific.

Ok, if you reload you browser now, you must see this picture:

The last step on this post is to also create a component for the main dynamic section representing the home page.
We will do this for the sake of modularity and to get ready for the next post about routing.
The template is very simple:
root-folder/app/views/home-page.html

<h2 class="text-center">  
    Search by Github username above
</h2>  

The component is also trivial:
root-folder/app/js/components/home-page.js

import Ractive from 'ractive';  
import Template from '../../views/home-page.html';

var HomePage = Ractive.extend({  
    template: Template
});

export default HomePage;  

Now update App to use this new component:
root-folder/app/views/app.html

<div class="main-container">  
    <nav class="navbar navbar-default" role="navigation">
        <div class="col-sm-1">
            <a href="/">
                <h3>Notetaker</h3>
            </a>
        </div>
        <div class="col-sm-7 col-sm-offset-1" style="margin-top: 15px;">
            <SearchUser placeholder="Type a GitHub username..." />
        </div>
    </nav>
    <div class="container">
        <HomePage />
    </div>
</div>  

root-folder/app/js/app.js

import Ractive from 'ractive';  
import template from '../views/app.html';  
import SearchUserComponent from './components/layout/search-user';  
import HomePageComponent from './components/home-page';

let App = new Ractive({  
  el: '#app',
  template: template,
  components: {
      SearchUser: SearchUserComponent,
      HomePage: HomePageComponent
  }
});

export default App;  


Summary

  • We now know what we want to build.
  • We created the main App instance using Ractive.
  • We configured webpack to make it load our html templates.
  • We got some insights about this library and how it works: what components are, how to configure data bindings, how to listen to DOM events and how to handle data contexts.
  • We divided our little application in pieces both in terms of presentation (views) and behavior (components).

You can check the source code in this GitHub repo.


Previous post: Setting up your environment
Next post: Routing