Components

Hello World - ECMAScript 6 - Building Virtual DOM Tree - Click Counter - Shortcut Methods - HTML Styles - Todo List - Mixins

Hello World

The following example will insert <div>Hello, World!</div> into the div#hello-message element.

index.html

<h1>Greeting from Cape.JS</h1>
<div id="hello-message" data-name="World"></div>

<script src="./hello_message.js"></script>
<script>
  var component = new HelloMesage();
  component.mount('hello-message');
</script>

hello_message.js

var HelloMesage = Cape.createComponentClass({
  render: function(m) {
    m.div('Hello, ' + this.root.data.name + '!')
  }
});

First of all, we must define the render method for Cape.JS components. The role of this method is to create a virtual DOM tree. Cape.JS updates the real DOM tree of browsers using this virtual tree.

The render method should take an argument, which is called markup builder. When you call its div method, a div node is added to the virtual DOM tree. The markup builder has corresponding methods for all valid tag names of HTML5, such as p, span, br, section, video, etc.

You can call this.root to get the node which the component was mounted on. And you can access to data-name attributes of the root node by this.root.data.name.

You can find the source code of working demo on https://github.com/capejs/capejs/tree/master/demo/hello_message.

ECMAScript 6

If you want to write more concisely, try to define class using ECMAScript 6 (ES6) syntax.

hello_message.es6

class HelloMessage extends Cape.Component {
  render(m) {
    m.div(`Hello ${this.root.data.name}!`)
  }
}

You can find the source code of working demo on https://github.com/capejs/capejs/tree/master/es6-demo/hello_message.

You must have npm and babel-core to see this demo page. You must also have browserify to convert .es6 file to .js file.

See https://github.com/capejs/capejs/tree/master/es6-demo/README.md for details.

Building Virtual DOM Tree

index.html

<h1>Greeting from Cape.JS</h1>
<div id="hello-message" data-name="World"></div>

<script src="./hello_message2.js"></script>
<script>
  var component = new HelloMesage2();
  component.mount('hello-message');
</script>

hello_message2.js

var HelloMesage2 = Cape.createComponentClass({
  render: function(m) {
    m.p(function(m) {
      m.text('Hello, ');
      m.strong(function(m) {
        m.text(this.root.data.name);
        m.text('!');
      })
    })
  }
});

This example will generate <p>Hello, <strong>World!</strong></p>.

Note that strong method takes a function, which create the content of strong element. In this way you can create a deeply-nested DOM tree.

With ES6 syntax, you can write much tersely:

class HelloMesage2 extends Cape.Component {
  render(m) {
    m.p(m => {
      m.text('Hello, ');
      m.strong(m => {
        m.text(this.root.data.name);
        m.text('!');
      })
    })
  }
}

All methods of markup builder can be chained. So, you can rewrite the above code as follows:

class HelloMesage2 extends Cape.Component {
  render(m) {
    m.p(m =>
      m.text('Hello, ').strong(m =>
        m.text(this.root.data.name).text('!');
      )
    )
  }
}

Note that you can omit braces when the arrow function has only a single expression. See MDN’s Arrow functions for details.

Click Counter

On this example, your will see the number which gets incremented each time you click on the surrounding div box.

index.html

<div id="click-counter"></div>

<script src="./click_counter.js"></script>
<script>
  var counter = new ClickCounter();
  counter.mount('click-counter');
</script>

click_counter.js

var ClickCounter = Cape.createComponentClass({
  render: function(m) {
    m.div(String(this.counter), {
      class: 'counter',
      onclick: function(e) { this.increment() }
    })
  },

  init: function() {
    this.counter = 0;
    this.refresh();
  },

  increment: function() {
    this.counter++;
    this.refresh();
  }
});

Note that we give the second argument to the div method:

{
  class: 'counter',
  onclick: function(e) { this.increment() }
}

This associative array represents the attributes of div element. We can attach a handler (function) to the click event for this element like this.

Within event handlers, this denotes the component itself. So you can call its increment method by this.increment().

A method call this.refresh() redraws the component. You should call it at the end of the init method, but if the component lacks the init method, the refresh method is called when the component is mounted.

You can find the source code of working demo on https://github.com/capejs/capejs/tree/master/demo/click_counter.

Shortcut Methods

Cape.JS provides some shortcut methods for the ease of programming, such as #class, #disabled, #onclick, etc. These methods preset the value of attributes for the element which will be created nextly.

In the previous example, we wrote like this:

    m.div(String(this.counter), {
      class: 'counter',
      onclick: function(e) { this.increment() }
    })

With shortcut methods, we can write it more concisely:

    m.class('count');
    m.onclick(function(e) { this.increment() });
    m.div(String(this.counter);

Because all methods of markup builder are chainable, you can also write like this:

    m.class('count')
      .onclick(function(e) { this.increment() })
      .div(String(this.counter);

The shortcut methods affect only the very element which will be created nextly. In the following example, the second <div> element will have no class attribute:

    m.class('greeting');
    m.div('Hello, World!');
    m.div('My name is Cape.JS.');

See MarkupBuilder#class() and MarkupBuilder#onclick() for details.

HTML Styles

When you want to change the style of an element, you have two choices. The first one is to give the style option to div method etc. The second one is to use the #css method to preset styles for the element which will be created nextly.

The following example creates a <span> element whose text is rendered with red color:

m.span('Hello, World!', style: { color: 'red' });

Note that you should give an object as the value of style option. You can not write like this:

m.span('Hello, World!', style: 'color: red');

Note also that you should use camel case for the style names:

m.span('Hello, World!', style: { fontWeight: 'bold' });

Using the #css method, you can rewrite the last example as follows:

m.css({ fontWeight: 'bold' }).span('Hello, World!');

Alternatively, you can pass two strings (style name and its value) to the #css method:

m.css('fontWeight', 'bold').span('Hello, World!');

Todo List

On this example, your can add a todo item from a HTML form and toggle the completed property of todo items by clicking check boxes.

index.html

<div id="todo-list"></div>

<script src="./todo_list.js"></script>
<script>
  var todoList = new TodoList();
  todoList.mount('todo-list');
</script>

todo_list.js

var TodoList = Cape.createComponentClass({
  render: function(m) {
    m.ul(function(m) {
      this.items.forEach(function(item) {
        this.renderItem(m, item);
      }.bind(this))
    });
    this.renderForm(m);
  },

  renderItem: function(m, item) {
    m.li(function(m) {
      m.label({ class: { completed: item.done }}, function(m) {
        m.onclick(function(e) { this.toggle(item) })
          .checked(item.done).input({ type: 'checkbox' })
        m.text(item.title);
      })
    })
  },

  renderForm: function(m) {
    m.onsubmit(function(e) { this.addItem(); return false; });
    m.formFor('item', function(m) {
      m.onkeyup(function(e) { this.refresh() }).textField('title');
      m.onclick(function(e) { this.addItem() })
        .disabled(this.val('item.title') === '').btn("Add");
    });
  },

  init: function() {
    this.items = [
      { title: 'Foo', done: false },
      { title: 'Bar', done: true }
    ];
    this.refresh();
  },

  toggle: function(item) {
    item.done = !item.done;
    this.refresh();
  },

  addItem: function() {
    this.items.push({ title: this.val('item.title'), done: false });
    this.val('item.title', '');
    this.refresh();
  }
});

Note that we use the formFor method of markup builder to render an HTML form. This method takes a string as the first argument, and sets the name attribute of the form to it.

The textFiled method creates an input element of the type text. If we give 'title' as the first argument of the method, the value of name attribute of this input element is set to item.title.

We can get its value by this.val('item.title'). You can also set its value with val method by giving a new value as the second argument.

You can find the source code of working demo on https://github.com/capejs/capejs/tree/master/demo/todo_list.

As for the #btn() method, see MarkupBuilder#btn().

Mixins

When you build a large application with many similar components, you will want to extract common methods to mixins, objects that contain a combination of methods.

The following example illustrates how to create mixins and incorporate them into component classes.

index.html

<div id="main"></div>

<script src="./form_controls.js"></script>
<script src="./simple_form.js"></script>
<script>
  var simple_form = new SimpleForm();
  simple_form.mount('main');
</script>

form_controls.js

var FormControls = {
  renderTextField: function(markup, fieldName, labelText) {
    markup.div({ class: 'form-group' }, function(m) {
      m.labelFor(fieldName, labelText);
      m.textField(fieldName, { class: 'form-control' });
    })
  },

  renderTextareaField: function(markup, fieldName, labelText) {
    markup.div({ class: 'form-group' }, function(m) {
      m.labelFor(fieldName, labelText);
      m.textareaField(fieldName, { class: 'form-control' });
    })
  },

  renderButtons: function(markup) {
    markup.div({ class: 'form-group' }, function(m) {
      m.btn('Submit', { class: 'btn btn-primary' });
      m.btn('Cancel', { class: 'btn btn-default' });
    })
  }
}

simple_form.js

var SimpleForm = Cape.createComponentClass({
  render: function(m) {
    m.p(function(m) {
      m.formFor('user', function(m) {
        this.renderTextField(m, 'family_name', 'Family Name');
        this.renderTextField(m, 'given_name', 'Given Name');
        this.renderTextareaField(m, 'remarks', 'Remarks');
        this.renderButtons(m);
      })
    })
  }
});

Cape.merge(SimpleForm.prototype, FormControls);

The Cape.merge method imports all properties (methods) of FormControls into SimpleForm.prototype.

Note that Cape.merge does not rewrite existing properties of SimpleForm.prototype so that you can override one or more methods of FormControls within the definition of SimpleForm class.

You can find the source code of working demo on https://github.com/capejs/capejs/tree/master/demo/mixins.