Components

If you've used Backbone Views in the past, you'll be familiar with the basic concept of extending the base class to create a new subclass with default data and additional methods.

Step 1

In this tutorial we're first going to learn about Ractive.extend and use it to create an image slideshow, using gifs from devopsreactions.tumblr.com.

We've got our basic template set up – we just need to make a few additions. First, we need to add a mustache for the image URL:

<div class='main-image'
     style='background-image: url("{{image.src}}");'>
</div>

We're using a CSS background rather than an img element for this example, because you can use the background-size: contain CSS rule to ensure that the image is shown at maximum size without distorting the aspect ratio.

Then, we need to add a mustache for the image caption:

<div class='caption'>
  <p>{{image.caption}}</p>
</div>

Finally, let's add some event expressions that we can fill in later:

<a class='prev' on-click='@.goto(current - 1)'><span>&laquo;</span></a>
<!-- ... -->
<a class='next' on-click='@.goto(current + 1)'><span>&raquo;</span></a>

Execute the JavaScript to redraw the view, with the placeholder data that's already there.

Step 2

Time to create our Slideshow class:

var Slideshow = Ractive.extend({
  // this will be applied to all Slideshow instances
  template: '#slideshow',

  // method for changing the currently displayed image
  goto: function ( imageNum ) {
    // goto method goes here...
  },

  // default data
  data: function () {
    // return the default data for the component here
  }
});

Each Slideshow instance will have a goto method in addition to the normal Ractive instance methods. We'll also provide default data for the component, notably to start with the current index as 0.

When providing data for components, you should always use a data function that returns a data dictionary. If you return the same object from each invocation, then all of your component instances will share the same underlying data, but the properties won't necessarily be kept up-to-date across instances.

Component data and instance data is combined into the final instance's data dictionary starting with Ractive default data, moving through the component hierarchy, and ending with the data supplied to the instance constructor.

Let's write our goto method:

function ( index ) {
  var images = this.get( 'images' );

  // handle wrap around
  var num = ( index + images.length ) % images.length;

  this.set( 'current', num );
}

Let's add some code to instantiate the slideshow with our gifs. There's a ready-made images variable you can use for this step:

var slideshow = Slideshow({
  target: '#target',
  data: { images: images }
});

Go ahead and execute the code – you should now have a working slideshow.

Needless to say, you could add as many bells and whistles as you wanted – fading or sliding transitions, image preloading, thumbnails, touchscreen gesture controls, and so on.

You could, of course, just use an existing image slideshow library. But then you would have to learn that library, and potentially submit to its design philosophy.

Ractive.js is all about flexibility. If you want to change the design or behaviour of a component (say, adding a class name to a particular element), the power to do so is in your hands – the template is easy to understand and tweak because it's basically just HTML, and the view logic is straightforward.

It's better to be able to build your own solution than to rely on developers maintaining high quality and up-to-date documentation.

Step 3

Now we have our lovely slideshow component, but suppose we want to use it in our Ractive.js app rather than mounting it directly on an element. It turns out that that is a pretty simple thing to accomplish. We can register our component with either globally or with our main Ractive.js instance, and anywhere that the template has a <slideshow /> element, Ractive.js will create an instance of Slideshow and mount it inline.

Ractive.components.slideshow = Slideshow;

// or
var ractive = Ractive({
    // ...

  components: {
    slideshow: Slideshow
  },

  // ...
});

Now in the template, we just reference the component as if it were a custom element:

<div style-height="40vh">
  <slideshow />
</div>

We were passing the list of pictures to the instance as it was being initialized, but since Ractive.js is now managing the instance, how do we get the list of pictures to the component instance? Well, we create a mapping from the data in the host instance to the images keypath in the component by using an attribute:

<div style-height="40vh">
  <slideshow images="{{devopsImages}}" />
</div>

Update the code with another images array and put two slideshow components in the main instance.

Mappings are automatically managed cross-instance links. A link is a bit like a filesystem symlink in that the data isn't copied anywhere - it just gets a new path that points to it. Changing the data in either place is effectively the same as changing it everywhere at once.

Step 4

We have our image slideshow usable from any app now, but what if we wanted to make it slightly more customizable? Perhaps we could allow the user to have a bit more control over the template by, say, letting them include some sort of disclaimer on all of the slides. We could hard-code the disclaimer in a special version for each client, but that sounds like it would be awful to maintain.

Fortunately, Ractive.js allows you to use the content of a component tag to pass partials to the component instance. Any content not within a {{#patial}} tag is collected up and exposed to the component as a partial named content. Any {{#partial}}s are supplied to the component instance with the names they are given in the {{#partial}} tag. We'll use a disclaimer partial for our component:

<slideshow>
  {{#partial disclaimer}}<div class="disclaimer">I don't know what we're disclaiming, but we're disclaiming it <a on-click="@.disclaim(), false" href="#">here</a>.</div>{{/partial}}
</slideshow>

We can add a reference to the partial in the Slideshow component template:

{{>disclaimer}}

It's not a bad idea to supply a default partial with the component, so that Ractive.js doesn't emit any warnings about missing partials. Any partials passed to the component will override any supplied to extend.

Now what will happen when you click the "here" link? Partials exist completely in the context of the instance in which they are used, and there is no disclaim method on the Slideshow component, so nothing will happen (except an error will be logged to the console). If you want a passed-in partial to render in the context from which it was passed, you can {{yield}} it.

{{yield disclaimer}}

Add a disclaim method to the main instance, update the Slideshow component, and run the code to see it all in action.

Since yield puts the partial in the context of the caller, there's not much point in using it within an #each block because the array being iterated is unreachable from the yield. yields can also supply aliases to data local to the component e.g. {{yield disclaimer with current as currentImage}}. yielding the content partial just requires dropping the name.

Step 5

Now suppose we wanted to do something to each of our component instances as they're created, like validate that they have certain parameters provided and issue a warning if they don't. Ractive.js components actually have a pretty complete set of lifecycle events that allow component authors to hook into just about every stage of a component's lifecycle - from construction to destruction hitting init, render, unrender, teardown, complete (render complete along with any transitions) and a few others in between. Each lifecycle event also happens to be an instance event.

Lets add our check to make sure the Slideshow component receives an image parameter. There are a few ways to do this, including using on and providing an oninit function as an initialization parameter.

var ractive = Ractive({
  // ...
  on: {
    init: function () {}
  },
  // ...
});

// or

var ractive = Ractive({
  // ...
  oninit: function () {},
  // ...
});

// for other lifecycle events, you can attach after init

ractive.on( 'render', function () {} );

Add an init event handler that checks to see if there are no images and issues a warning if there are none.

Step 6

When we started out with our slideshow, we were rendering it directly to a target element rather than as a component in another Ractive.js instance. It turns out that having self-contained components like that is a pretty convenient pattern for managing complexity in a larger app. All of the related functionality for a feature or group of features can be grouped into one Ractive.js extension known as a view. Each individual view can then be loaded and/or rendered independently.

In order to use views with a main instance controlling the overall app, you would have to have some sort of big #if/else block with each view included as a branch. You could also resort to some sort of partial generation scheme. There's an easier way though.

Ractive.js will allow you to attach one independent instance to another using attachChild, optionally specifying a target. If you don't specify a target, then the child instance will not be rendered, but if you do specify a target, then the instance will be rendered into the first available matching anchor. An anchor looks like a component or element, but its name always starts with a #. You may have as many anchors as you like, and they may each have the same or different names.

<#anchor />
<#ICallThisOneStan>
  Stan has a content partial.
  {{#partial name}}He also has a name partial.{{/partial}}
</ICallThisOneStan>

Go ahead and fill out the two provided views as you like, add anchors to the main instance template, and attach an instance of each view to an anchor on the main instance.

You can detach a child using the conveniently named detachChild method.

Child instances can be attached in prepend, insertAt, or append (the default) mode. Ractive.js will try to find a matching anchor for each child starting with the first. If there aren't enough anchors, some instances will not be rendered. Each time a child is attached or detached, Ractive.js will adjust any affected anchors so all instances that can be rendered are rendered.

Each anchor has its own list of children associated with it, which is what the attachment modes are referreing to - prepend will insert a child at the front, append at the end, and insertAt at the specified index. The list of children for a particular anchor, say <#main />, is kept up to date in an observable way so that you can automatically generate anchors as components are attached using {{#each @.children.byName.main}}<#main />{{/each}}.

An anchored attached child is effectively a component that the host instance doesn't control.