Tutorial overview

What follows is a 10-minute tutorial on learning the basic structure of a skit project. We will build an example site with two pages, sharing a common API layer and base page style.

If you just want to check out the finished code, here it is:

Browse the finished tutorial code

… check out the other examples in example also, including the code for this site.

Step 1: Install

  1. Install Node v0.10.x
  2. Install skit from npm:
     $ npm install skit

    (We will need .bin/skit from your node_modules directory in the next step. If you have a separately configured npm prefix, ignore the wonky ./node_modules/.bin part below.)

Step 2: Minimum viable skit project

  1. First we will create a new directory with a “public” directory inside:
    $ mkdir helloworld
    $ mkdir helloworld/public
    $ touch helloworld/public/Home.js 
  2. Now open helloworld/public/Home.js in your favorite editor, and paste the following:
    // Load the Controller module from the skit library.
    var Controller = skit.platform.Controller;
    // Create a new Controller subclass and set it as this module's exports.
    module.exports = Controller.create({
      __body__: function() {
        return '<p>Hello world!</p>';
      }
    });
  3. Run the skit server:
    $ ./node_modules/.bin/skit run helloworld --debug
    Skit server listening on 0.0.0.0:3001
  4. Now you can visit http://localhost:3001 in a browser to see “Hello world!”
  5. Inspect the source to see that <p>Hello world!</p> is not the entire body of the response, rather it is inserted into the <body> followed by code to load the appropriate JavaScript resources from the skit server as well, including the Home.js file you just wrote.

Step 3: Templates

Expanding on the MVP, we can add more files to organize the project a bit better.

  1. Create Home.html in the same directory as Home.js. Files sharing the same prefix (anything before the first underscore) become part of the same skit module.
    <p>Hello, world! I’m a template.</p>
    <p>Here’s a random number: {{ random }}</p>
  2. Update Home.js to match the following:
    var Controller = skit.platform.Controller;
    // Loads the "Home.html" file, a Handlebars template.
    var template = __module__.html;
    module.exports = Controller.create({
      __title__: function() {
        return 'Home';
      },
      __body__: function() {
        // Handlebars templates are functions that return HTML.
        return template({random: Math.random()});
      }
    }); 
  3. Reload http://localhost:3001 to see the changes: Now there should be a <title> on the page, and you should see a random number output on the page that’s different with every page load.

Step 4: Getting started on the client side

Now that we have templates, we can easily add more markup and will quickly need to start dealing with it. You can use whatever library you want for client-side stuff — you might’ve heard of one or two options.

That being said, skit includes a simple DOM and events library, because the public demanded more.

  1. Add a button with id="reload" to Home.html:
    <p>
      Hello, world! I’m a template.
      <button id="reload">Reload</button>
    </p>
    <p>Here’s a random number: {{ random }}</p>
  2. Update Home.js to listen for clicks on id="reload":
    // Load DOM and events from skit.browser.
    var dom = skit.browser.dom;
    var events = skit.browser.events;
    // Includes we already needed.
    var Controller = skit.platform.Controller;
    var template = __module__.html;
    module.exports = Controller.create({
      __title__: function() {
        return 'Home';
      },
      __body__: function() {
        return template({random: Math.random()});
      },
      __ready__: function() {
        // dom.get() returns a matching element, or null if none is found.
        var reload = dom.get('#reload');
        events.bind(reload, 'click', this.reload, this);
      }
    }); 
  3. Reload http://localhost:3001 to see the changes: Now we have a button, and when you click it, a new number appears.

Step 5: Loading HTTP API content

OK, now for the cool part. Let’s load some content over the network and include it in our server-side rendered response. GitHub has been gracious enough to allow us to access their JSON API without authentication — yay!

To plug this in, we’ll need a new module in skit.platform called skit.platform.net.

  1. Update Home.js to include the net module and load some content:
    // Includes we already needed.
    var dom = skit.browser.dom;
    var events = skit.browser.events;
    var Controller = skit.platform.Controller;
    // Add skit.platform.net for making HTTP requests.
    var net = skit.platform.net;
    var template = __module__.html;
    // This GitHub API returns a list of public gists.
    var GITHUB_URL = 'https://api.github.com/gists/public';
    module.exports = Controller.create({
      __preload__: function(loaded) {
        net.send(GITHUB_URL, {
          success: function(response) {
            this.gists = response.body;
          },
          error: function(response) {
            console.log('Error loading:', response.code,
                'body:', response.body);
          },
          complete: loaded,
          context: this
        });
      },
      __title__: function() {
        return 'Home';
      },
      __body__: function() {
        return template({gists: this.gists});
      },
      __ready__: function() {
        var reload = dom.get('#reload');
        events.bind(reload, 'click', this.reload, this);
      }
    }); 
  2. Update Home.html to output the gist items:
    <p>
      Hello, world! I’m a template.
      <button id="reload">Reload</button>
    </p>
    <ul>
    {{#each gists }}
      <li>
        <a href="{{ html_url }}">
          {{#each files }}
            {{ filename }}
          {{/each}}
        </a>
        {{#if owner.login }}
          by <a href="{{ owner.html_url }}">{{ owner.login }}</a>
        {{/if}}
      </li>
    {{/each}}
    </ul>
  3. Reload http://localhost:3001 to see the changes. Now we have a list of links, generated server-side, to gists returned by the GitHub API. Pressing “Reload” will reload the data from the client side and rerender the page with the new content, if there is any.

Step 6: Template partials

OK, let’s clean this up for a second.

  1. Update Home.html to reference a partial:
    <p>
      Hello, world! I’m a template.
      <button id="reload">Reload</button>
    </p>
    <ul>
    {{#each gists }}
      {{> __module__.item.html }}
    {{/each}}
    </ul>
  2. Create Home_item.html in the same directory as Home.html:
    <li>
      <a href="{{ html_url }}">
        {{#each files }}
          {{ filename }}
        {{/each}}
      </a>
      {{#if owner.login }}
        by <a href="{{ owner.html_url }}">{{ owner.login }}</a>
      {{/if}}
    </li>
  3. Reload http://localhost:3001 — it should look the same. Automatic partials! Note that you can also reference this template from the JavaScript file as __module__.item.html in case you want to replace an individual <li> or somesuch.

Step 7: Library modules

So far we’ve only used files in public, which is actually a special directory in skit — public is the base of our public URL structure. (More on that later.)

Let’s move the API code into a new module, called GitHubAPIClient, and then include it in Home.js.

  1. Update Home.js to reference a module:
    var dom = skit.browser.dom;
    var events = skit.browser.events;
    var Controller = skit.platform.Controller;
    // Add GitHubAPIClient for loading GitHub content.
    var GitHubAPIClient = library.GitHubAPIClient;
    var template = __module__.html;
    module.exports = Controller.create({
      __preload__: function(loaded) {
        GitHubAPIClient.loadGists(function(gists) {
          this.gists = gists;
          loaded();
        }, this);
      },
      __title__: function() {
        return 'Home';
      },
      __body__: function() {
        return template({gists: this.gists});
      },
      __ready__: function() {
        var reload = dom.get('#reload');
        events.bind(reload, 'click', this.reload, this);
      }
    });
  2. Create a new directory: library, and add a new file, GitHubAPIClient.js.
    $ mkdir helloworld/library
    $ touch helloworld/library/GitHubAPIClient.js
  3. Edit GitHubAPIClient.js and add the new function we’re calling from Home.js:
    var net = skit.platform.net;
    var GITHUB_URL = 'https://api.github.com/gists/public';
    
    var logError = function(response) {
      console.log('Error loading:', response.code,
          'body:', response.body);
    };
    
    module.exports = {
      loadGists: function(apiCallback, context) {
        var gists = [];
        var done = function() {
          apiCallback.call(context, gists);
        };
        net.send(GITHUB_URL, {
          success: function(response) {
            gists = response.body;
          },
          error: logError,
          complete: done
        });
      }
    };
  4. Reload http://localhost:3001 — it should look the same for now, but we’re about to add another page that will also use the same shared library module.

Step 8: A detail page

In step 7 I mentioned the public directory’s structure dictated the URLs we handle, so let’s add another directory inside to see what I mean.

  1. Create a new directory gist, and add two new files, Gist.js and Gist.html:
    $ mkdir helloworld/public/gist
    $ touch helloworld/public/gist/Gist.js
    $ touch helloworld/public/gist/Gist.html
  2. Update Home_item.html to reference the new detail page:
    <li>
      <a href="/gist?id={{ id }}">
        {{#each files }}
          {{ filename }}
        {{/each}}
      </a>
      {{#if owner.login }}
        by <a href="{{ owner.html_url }}">{{ owner.login }}</a>
      {{/if}}
    </li>
  3. Update GitHubAPIClient.js to add a method for fetching a single gist:
    var dom = skit.browser.dom;
    var events = skit.browser.events;
    var Controller = skit.platform.Controller;
    var net = skit.platform.net;
    var GITHUB_BASE_URL = 'https://api.github.com/gists/';
    var logError = function(response) {
      console.log('Error loading:', response.code,
          'body:', response.body);
    };
    module.exports = {
      loadGists: function(apiCallback, context) {
        var gists = [];
        var done = function() {
          apiCallback.call(context, gists);
        };
        net.send(GITHUB_BASE_URL + 'public', {
          success: function(response) {
            gists = response.body;
          },
          error: logError,
          complete: done
        });
      },
      loadGist: function(gistId, apiCallback, context) {
        var gist = null;
        var done = function() {
          apiCallback.call(context, gist);
        };
        net.send(GITHUB_BASE_URL + encodeURIComponent(gistId), {
          success: function(response) {
            gist = response.body;
          },
          error: logError,
          complete: done
        })
      }
    };
  4. Fill in Gist.html:
    <a href="/">&larr; Home</a>
    
    <h1>Gist by {{ gistOwner }}</h1>
    
    <h2>Gist description</h2>
    <p>{{ gist.description }}</p>
    <p><a href="{{ gist.html_url }}">View on GitHub</a></p>
    
    {{#each gist.files }}
      <h3>{{@key}}</h3>
      <p>{{ size }} bytes, {{ type }} ({{ language }})</p>
      <pre>{{ content }}</pre>
    {{/each}}
  5. Fill in Gist.js:
    var Controller = skit.platform.Controller;
    // skit.platform.string gives us string utilities.
    var string = skit.platform.string;
    // skit.platform.navigation tells us about the current URL.
    var navigation = skit.platform.navigation;
    var GitHubAPIClient = library.GitHubAPIClient;
    var template = __module__.html;
    module.exports = Controller.create({
      __preload__: function(loaded) {
        var query = navigation.query();
        GitHubAPIClient.loadGist(query['id'], function(gist) {
          if (!gist) {
            // On the server side, this issues a 404.
            navigation.notFound();
          } else {
            this.gist = gist;
          }
    
          loaded();
        }, this);
      },
      __title__: function() {
        return 'Gist by ' + string.escapeHtml(this.gistOwner());
      },
      __body__: function() {
        return template({gist: this.gist, gistOwner: this.gistOwner()});
      },
      gistOwner: function() {
        if (this.gist['owner']) {
          return this.gist['owner']['login'];
        }
        return 'anonymous';
      }
    });
  6. Reload http://localhost:3001 and click on one of the gist URLs to see the new subpage in action. Note that an invalid ?id= passed in the URL results in a server-side 404.

Step 9: Base Controller

Even though Home.js and Gist.js both exist in the same website, they don’t share any behavior yet. For this, we can introduce hierarchy to Controller classes.

  1. Add three new files, BaseController.js, BaseController.css, and BaseController.html, in library:
    $ touch helloworld/library/BaseController.js
    $ touch helloworld/library/BaseController.html
    $ touch helloworld/library/BaseController.css
  2. Fill in BaseController.html:
    <div id="content">
      <!-- Triple curlies inserts HTML unescaped. -->
      {{{ childHtml }}}
    </div>
  3. Fill in BaseController.js:
    var Controller = skit.platform.Controller;
    var template = __module__.html;
    module.exports = Controller.create({
      __title__: function(childTitle) {
        // Parents get the title generated by children as an argument.
        return childTitle + ' | Hello World';
      },
      __body__: function(childHtml) {
        // Parents get the HTML generated by children as an argument.
        return template({childHtml: childHtml});
      },
      __ready__: function() {
        // Parents also get __preload__, __load__ and __ready__ calls.
      }
    });
  4. Fill in BaseController.css:
    /* CSS files are automatically included along with modules. */
    body {
      font-family: arial, sans-serif;
      font-size: 14px;
    }
    
    #content {
      max-width: 640px;
      margin: 20px auto;
    }
  5. Add to the top of public/Home.js and public/gist/Gist.js:
    var BaseController = library.BaseController;
  6. Update public/Home.js and public/gist/Gist.js’s Controller.create() to look like this:
    // Controller.create's first argument becomes the parent class.
    module.exports = Controller.create(BaseController, {
  7. Reload http://localhost:3001 to see the new shared container for the main page and detail page — they should both have a max-width and share a new sans-serif font.

Fin.

For more, check out the full API Reference and have a look at the project on GitHub.

Browse the finished tutorial code