CanJS - Component

canjs

https://matthewphillips.info/posts/building-an-accordion-with-can-component.html

What is the purpose of can.Component?

The purpose of can.Component is to build individual portable and re-usable components using custom tags. When we define a component, we specify a custom tag, and when CanJS see that custom tag inside a template file, the component that we defined is instantiated. A component's template is rendered as the innerHTML of the custom tag of the component. For example, the following component:

can.Component.extend({
    tag: "hello-world",
    template: can.stache("<h1>Hello World</h1>")
});

will changes <hello-world></hello-world> elements into:

<hello-world><h1>Hello World</h1></hello-world>

How can we extend can.Component?

To create a can.Component, you must first extend can.Component with the methods and properties of how your component behaves:

can.Component.extend({
    tag: "hello-world",
    template: can.stache("{{#if visible}}{{message}}{{else}}Click me{{/if}}"),
    viewModel: {
        visible: false,
        message: "Hello There!"
    },
    events: {
        click: function(){
            this.viewModel.attr("visible", !this.viewModel.attr("visible") );
        }
    }
});

Use can.Component.extend to create a can.Component constructor function that will automatically get initialized whenever the component's tag is found. Note that inheriting from components works differently than other CanJS APIs. You can't call .extend on a particular component to create a "subclass" of that component. Instead, components work more like HTML elements. To reuse functionality from a base component, build on top of it with parent components that wrap other components in their template and pass any needed viewModel properties via attributes.

What are some of the things that I should remember about the viewModel object?

can.Component.extend({
    template: can.stache("<h2 can-click='makeActive'>{{title}}</h2>"+
        "{{#if active}}<content></content>{{/if}}"),
    tag:"panel",
    viewModel: {
        active: false
    },
    events: {
        inserted: function() {
            this.element.parent().viewModel().addPanel( this.viewModel );
        },
        removed: function() {
            this.element.parent().viewModel().removePanel( this.viewModel );
        }
    }
});
<div id="out"></div>
<script id="app" type="text/stache">
    <accordion>
        <panel title="Contacts">
            <button>Change title attribute to "Users"</button>
            <ul>
                <li>Justin</li>
                <li>Brian</li>
            </ul>
        </panel>
        <panel title="Libraries">
            <ul>
                <li>CanJS</li>
                <li>JavaScriptMVC</li>
            </ul>
        </panel>
    </accordion>
</script>

The above code define a component for the panel custom tag. The title attribute from the custom tag is automatically added to the component's view model. The template defined in the component reference the {{title}} from the view model, which is the same as the title attribute of the custom tag panel, and changing the value of the title attribute in the custom tag automatically update the view model.

<div id="out"></div>

can.Component.extend({
    tag: "my-paginate",
    viewModel: {
        offset: 0,
        limit: 20,
        next: function() {
            this.attr("offset", this.offset + this.limit);
        },
        page: function(){
            return Math.floor(this.attr('offset') / this.attr('limit')) + 1;
        }
    },
    template: can.stache("Page {{page}} <button ($click)='next()'>Next</button>")
});

$("#out").html(can.stache("<my-paginate/>")({}));

Look at the button element inside component's template, and see that the click event is associate with the next function defined in the view model.

Using html attributes like can-EVENT-METHOD, you can directly call a viewModel method from a template. The viewModel methods get called back with the current context, the element that you are listening to and the event that triggered the callback.

How can we user can.Component with IE8?

While CanJS does support Internet Explorer 8 out of the box, if you decide to use can.Component then you will need to include HTML5 Shiv in order for your custom tags to work properly. For namespaced tag names (e.g. <can:example>) and hyphenated tag names (e.g. <can-example>) to work properly, you will need to use version 3.7.2 or later.

How can we position the custom tag's source HTML?

Use the <content/> tag to position the custom element's source HTML. The following component:

can.Component.extend({
    tag: "hello-world",
    template: can.stache("<h1><content/></h1>")
});

Changes <hello-world>Hi There</hello-world> into:

<hello-world><h1>Hi There</h1></hello-world>

So, it looks that the custom tag can have some content before the component is instantiated can be retained and re-positioned.

How can we define a can.Component?

var people = [
    {firstname: "John", lastname: "Doe"},
    {firstname: "Emily", lastname: "Dickinson"},
    {firstname: "William", lastname: "Adams"},
    {firstname: "Stevie", lastname: "Nicks"},
    {firstname: "Bob", lastname: "Barker"}
];

can.Component.extend({
    tag: 'people',
    template: '<ul>' +
                '{{#each people}}' +
                '<li can-click="remove">' +
                    '{{lastname}}, {{firstname}}' +
                '</li>' +
                '{{/each}}' +
                '</ul>',
    scope: {
        people: people,
        remove: function( person ) {
            var people = this.attr("people");
            var index = people.indexOf(person);
            people.splice(index, 1);
        }
    }
});

var frag = can.view("app-template", {people: people});
$("#my-app").html(frag);
<script type="text/mustache" id="app-template">
    <people></people>
</script>
<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>

In the above code, when we use can.Component.extend, we specify a custom tag 'people', an in-line template string, and a scope object, and then in our main template, we use '<people></people>'. A component is created wherever <people></people> appears in a template.

The scope object on a Component contains the component's state, data, and behavior. Here, it specifies how to remove a person from the list.

The template for the component itself is passed via the template property. This can either be an external file or a string. Each li uses can-click, which declares an event binding. Here, remove inside the component's scope will be called with the relevant people object as an argument.

can.Component.extend({
    tag: "hello-world",
    template: can.stache("{{#if visible}}{{message}}{{else}}Click me{{/if}}"),
    viewModel: {
        visible: false,
        message: "Hello There!"
    },
    events: {
        click: function(){
            this.viewModel.attr("visible", !this.viewModel.attr("visible") );
        }
    }
});

How can we build a tab widget using can.Component?

<tabs>
    <panel title="Fruit">Oranges, Apples, Pears</panel>
    <panel title="Vegetable">Carrot, Lettuce, Rutabega</panel>
    <panel title="Grains">Bread, Pasta, Rice</panel>
</tabs>
var TabsViewModel = can.Map.extend({
    panels: [],
    active: null,
    addPanel: function( panel ){
        var panels = this.attr("panels");
        panels.push(panel);
        panel.attr("visible", false);
        //activate panel if it is the first one
        if ( panels.attr("length") === 1 ){
            this.activate( panel );
        }
    },
    removePanel: function( panel ){
        var panels = this.attr("panels");
        var index = panels.indexOf(panel);
        panels.splice(index, 1);
        //activate a new panel if panel being removed was the active panel
        if( this.attr("active") === panel ){
            panels.attr("length") ? this.activate(panels[0]) : this.attr("active", null)
        }
    },
    activate: function( panel ){
        var active = this.attr("active")
        if( active !== panel ){
            active && active.attr("visible", false);
            this.attr("active", panel.attr("visible", true));
        }
    }
})

can.Component.extend({
    tag: "tabs",
    scope: TabsViewModel,
    template: "<ul>\
    {{#each panels}}\
    <li can-click='activate'>{{title}}</li>\
    {{/each}}\
    </ul>\
    <content />"
});

can.Component.extend({
    tag: "panel",
    template: "{{#if visible}}<content />{{/if}}",
    scope: {
        title: "@"
    },
    events: {
        inserted: function() {
            this.element.parent().scope().addPanel( this.scope )
        },
        removed: function() {
            this.element.parent().scope().addPanel( this.scope )
        }
    }
});

var frag = can.view("app-template", {});
$("#my-app").html(frag);
<script type="text/mustache" id="app-template">
<tabs>
    <panel title="Fruit">Oranges, Apples, Pears</panel>
    <panel title="Vegetable">Carrot, Lettuce, Rutabega</panel>
    <panel title="Grains">Bread, Pasta, Rice</panel>
</tabs>
</script>

<!-- CanJS needs a place to put your application -->
<div id="my-app"></div>

In the above code, we defined two components, the 'tabs' component and the 'panel' component. And as before, in order to implement the component, we’ll define the observable view model—the scope object of the UI element. The TabsViewModel needs:

  1. An observable list of panels
  2. A state variable with the active panel
  3. Helper methods to add, remove, and activate panels

Since TabsViewModel is a can.Map, the panels property is automatically converted to a can.List. The active property references the panel object that should currently be displayed.

Now that the view model is defined, making a component is simply a matter of defining the way the tabs widget is displayed. The template for a tabs component needs a list of panel titles that will activate that panel when clicked. By calling activate with a panel as the argument, the properties of the panel can be manipulated. By changing the visible property of a panel, a template can be used to display or hide the panel accordingly.

The template object on the tabs component's scope needs to be able to render the content that is inside of the <tabs> tag. To do this, we simply use the <content> tag, which will render everything within the component's tags.

The tabs component contains panels, which are also defined as components. The tabs template contains the logic for whether the panel is visible (visible is controlled by the tabs component's activate method).

Each panel's scope contains a title, which should be taken from the title attribute in the <panel> tag. If you want to set the string value of a Component's attribute as a scope variable, use @.

In addition to the scope property, a component has an events property. This events property uses a can.Control instantiated inside the component to handle events.

Since we defined behavior for adding panels on the parent tabs component, we should use this method whenever a panel is inserted into the page (and an inserted event is triggered). To add the panel to the tabs component's scope, we call the addPanel method by accessing the parent scope with this.element.parent().scope()

With this component, any time a <tabs> element with <panel> elements is put in a page, a tabs widget will automatically be created.

How does inheriting from components works differently than other CanJS APIs?

Note that inheriting from components works differently than other CanJS APIs. You can't call .extend on a particular component to create a "subclass" of that component. Instead, components work more like HTML elements. To reuse functionality from a base component, build on top of it with parent components that wrap other components in their template and pass any needed viewModel properties via attributes.

How can we set the string value of an attribute on the view model using HTML syntax?

If you want to set the string value of the attribute on viewModel, set an attribute without any binding syntax. The following template, with the previous "hello-world" component:

var template = can.stache("<hello-world message='Howdy'/>");
template({});
can.Component.extend({
    tag: "hello-world",
    template: can.stache("<h1>{{message}}</h1>"),
    viewModel: {
        message: "Hi"
    }
});

renders to:

<hello-world><h1>Howdy</h1></hello-world>

Look at the template inside the can.Component.extend call. It is <h1>{{message}}</h1>, and the message is defined as "Hi" in the viewModel, so it should produce <hello-world><h1>Howdy</h1></hello-world>. However, message is defined as "Howdy" in the template outside of the component, as an attribute of the component's custom tag set the value for that attribute in the view model, effectively change the value of the message attribute from "Hi" to "Howdy", and change the output to <hello-world><h1>Howdy</h1></hello-world>

How can we define event handlers for our components?

A component's events object is used to listen to events (that are not listened to with view bindings). The following component adds "!" to the message every time <hello-world> is clicked:

can.Component.extend({
    tag: "hello-world",
    template: can.stache("<h1>{{message}}</h1>"),
    events: {
        "click" : function(){
            var currentMessage = this.viewModel.attr("message");
            this.viewModel.attr("message", currentMessage+ "!")
        }
    }
});
can.Component({
    events: {
        ".next click": function(){
            this.viewModel.next()
        }
    },
    viewModel: {
        next: function(){
            this.attr("offset", this.offset + this.limit);
        }
    }
});

A component's events object is used as the prototype of a can.Control. The control gets created on the component's element. The component's viewModel is available within event handlers as this.viewModel.

The events object can also listen to objects or properties on the component's viewModel. For instance, instead of using live-binding, we could listen to when offset changes and update the page manually:

can.Component.extend({
    tag: "my-paginate",
    viewModel: {
        offset: 0,
        limit: 20,
        next: function(){
            this.attr("offset", this.offset + this.limit);
        },
        page: function(){
            return Math.floor(this.attr('offset') / this.attr('limit')) + 1;
        }
    },
    template: can.stache("Page <span>1</span> <button class='next'>Next</button>"),
    events: {
        ".next click": function(){
            this.viewModel.next();  
        },
        "{scope} offset": function(){
            this.element.find("span").text( this.viewModel.page() )
        }
    }
})

$("#out").html(can.stache("<my-paginate/>")({}))

Components have the ability to bind to special inserted and removed events that are called when a component's tag has been inserted into or removed from the page.

events: {
    "inserted": function(){
        // called when the component's tag is inserted into the DOM 
    },
    "removed": function(){
        // called when the component's tag is removed from the DOM 
    }
}

What is the purpose of the helper object when used with can.Component.extend?

can.Component.extend({
    tag: "hello-world",
    template: can.stache("{{#isFriendly message}}"+
        "<h1>{{message}}</h1>"+
        "{{/isFriendly}}"),
    helpers: {
        isFriendly: function(message, options){
            if( /hi|hello|howdy/.test(message) ) {
                return options.fn();
            } else {
                return options.inverse();
            }
        }
    }
});

It defines helper functions. The helpers are only available within the component's template and source html. can.Component's helper object lets you provide helper functions that are localized to the component's template. Helpers are always called back with this as the viewModel.

What is the purpose of the leakScope object of can.Component.extend?

The documentation is very vague on this and this may change. See the official documentation.

Can we create a can.Component without using can.Component.extend?

Yes.

<div id="out"></div>
<script id="source-template" type="text/stache">
  <header>
    <my-greeting>
    {{site}} - {{title}}
    </my-greeting>
  </header>
</script>

can.Component({
    "tag": "my-greeting",
    template: can.stache("<h1><content/></h1>"),
    viewModel: {
        title: "can.Component"
    }
});

$("#out").html( can.view("source-template",{site: "CanJS"}) );

// Produce:
<header>
    <my-greeting>
        <h1>CanJS - can.Component</h1>
    </my-greeting>
</header>

Notice that "can.Component" comes from the viewModel, but "CanJS" comes from the second parameter to can.view(). Notice that the original content is able to access data from the source data and from the component's viewModel.

How can we push events onto the view model?

viewModels are usually can.Map objects, and Maps can publish events on themselves.

<script id="player-list-stache" type="text/stache">
  <ul>
      {{#each players}}
          <li {{#isEditing(.)}}class="editing"{{/isEditing}}
              ($click)='editPlayer(.)'>{{name}}</li>
      {{/each}}
  </ul>
  <player-edit 
      (close)="removeEdit()" 
      {player}="editingPlayer"/>
</script>
<script id="player-edit-stache" type="text/stache">
    {{#if player}}
        <input {($value)}="player.name"/>
        <button ($click)="close()">X</button>
    {{/if}}
</script>
import Component from "can/component/";
import stache from "can/view/stache/";
import $ from "jquery";

can.Component.extend({
    tag: "player-list",
    template: can.view('player-list-stache'),
    viewModel: {
        players: new can.List([{name: "Justin"},{name: "Brian"}]),
        editPlayer: function(player){
            this.attr("editingPlayer", player);
        },
        removeEdit: function(){
            this.removeAttr("editingPlayer");
        },
        isEditing: function(player){
            return this.attr("editingPlayer") === player;
        }
    }
});

can.Component.extend({
    tag: "player-edit",
    template: can.view('player-edit-stache'),
    viewModel: {
        close: function(){
            this.dispatch("close");
        }
    }
});

$("#out").html(stache("<player-list/>")({}));

Look at the template defined by player-edit-stache. There is a button with the click event. The click event is associated with the close function inside the player-edit component, which somehow dispatch the event to the parent component. And in the player-list-stache template, the play-edit tag has a (close) thing that is associated with the removeEdit function in the player-list component's view model.

Look at the template defined by player-list-stache. It define a <ul> list, with each <li> having a click event that is associated with the editPlayer function in the player-list component's view model. When the user click on the name of a player, editPlayer is invoked, which set the editingPlayer attribute in the player-list component's view model. Change in the attribute in the view model somehow cause the view / template ( player-list-stache) to be refresh or re-evaluated, which also contain the player-edit custom tag has a player attribute that is associated with the editingPlayer attribute in the parent's view model.

When the user click on the close button, which cause the removeEdit function to be invoked, which remove the editingPlayer attribute from the view model, which cause the view / template (layer-list-stache) to be refresh or re-evaluated, and because the editingPlayer attribute was removed, the player attribute of the player-edit custom tag is no longer defined, which cause the player-edit-stache template to be re-evaluated, and because the player attribute is not define, this template evaluate to an empty string, effectively remove the close button and the input box from the DOM.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License