DoneJS StealJS jQuery++ FuncUnit DocumentJS
4.3.0
5.0.0 3.13.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
  • API Docs
    • Observables
      • can-bind
      • can-compute
      • can-debug
      • can-define
      • can-define/list/list
      • can-define/map/map
      • can-define-backup
      • can-define-stream
      • can-define-stream-kefir
      • can-event-queue
      • can-kefir
      • can-list
      • can-map
      • can-map-define
      • can-observation
      • can-observation-recorder
      • can-observe
      • can-simple-map
      • can-simple-observable
      • can-stream
      • can-stream-kefir
      • can-value
    • Data Modeling
      • can-connect
      • can-connect-feathers
      • can-fixture
      • can-fixture-socket
      • can-ndjson-stream
      • can-set
    • Views
      • can-component
        • static
          • extend
        • prototype
          • ViewModel
          • events
          • helpers
          • leakScope
          • tag
          • view
          • viewModel
        • elements
          • <can-slot>
          • <can-template>
          • <content>
        • lifecycle hooks
          • connectedCallback
        • special events
          • beforeremove
      • can-stache
      • can-stache-bindings
      • can-stache-converters
      • can-stache-route-helpers
      • can-view-autorender
      • can-view-callbacks
      • can-view-import
      • can-view-live
      • can-view-model
      • can-view-nodelist
      • can-view-parser
      • can-view-scope
      • can-view-target
      • react-view-model
      • react-view-model/component
      • steal-stache
    • Routing
      • can-deparam
      • can-param
      • can-route
      • can-route-hash
      • can-route-mock
      • can-route-pushstate
    • JS Utilities
      • can-assign
      • can-define-lazy-value
      • can-diff
      • can-globals
      • can-join-uris
      • can-key
      • can-key-tree
      • can-make-map
      • can-parse-uri
      • can-queues
      • can-string
      • can-string-to-any
      • can-util
      • can-zone
      • can-zone-storage
    • DOM Utilities
      • can-ajax
      • can-attribute-encoder
      • can-child-nodes
      • can-control
      • can-dom-data
      • can-dom-events
      • can-dom-mutate
      • can-event-dom-enter
      • can-event-dom-radiochange
      • can-fragment
    • Data Validation
      • can-define-validate-validatejs
      • can-validate
      • can-validate-interface
      • can-validate-legacy
      • can-validate-validatejs
    • Typed Data
      • can-cid
      • can-construct
      • can-construct-super
      • can-data-types
      • can-namespace
      • can-reflect
      • can-reflect-dependencies
      • can-reflect-promise
      • can-types
    • Polyfills
      • can-symbol
      • can-vdom
    • Core
    • Infrastructure
      • can-global
      • can-test-helpers
    • Ecosystem
    • Legacy
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

can-component

  • npm package badge
  • Star
  • Edit on GitHub

Create a custom element that can be used to manage widgets or application logic.

<TAG BINDINGS...>[TEMPLATES][LIGHT_DOM]</TAG>

Create an instance of a component on a particular tag in a can-stache view. Use the bindings syntaxes to set up bindings.

The following creates a my-autocomplete element and passes the my-autocomplete’s ViewModel the Search model as its source property and a <can-template> that is used to render the search results:

<my-autocomplete source:from="Search">
    <can-template name="search-results">
        <li>{{name}}</li>
    </can-template>
</my-autocomplete>

Parameters

  1. TAG {String}:

    An HTML tag name that matches the tag property of the component. Tag names should include a hyphen (-) or a colon (:) like: acme-tabs or acme:tabs.

  2. BINDINGS {can-stache-bindings}:

    Use the following binding syntaxes to connect the component’s ViewModel to the view’s scope:

    • toChild:from=expression — one-way data binding to child
    • toParent:to=expression — one-way data binding to parent
    • twoWay:bind=expression — two-way data binding child to parent
    • on:event=expression — event binding on the view model
  3. TEMPLATES {sectionRenderer(context, helpers)}:

    Between the starting and ending tag can exist one or many <can-template> elements. Use <can-template> elements to pass custom templates to child components. Each <can-template> is given a name attribute and can be rendered by a corresponding <can-slot> in the component’s view.

    For example, the following passes how each search result should look and an error message if the source is unable to request data:

    <my-autocomplete source:from="Search">
        <can-template name="search-results">
            <li>{{name}}</li>
        </can-template>
        <can-template name="search-error">
            <div class="error">{{message}}</div>
        </can-template>
    </my-autocomplete>
    
  4. LIGHT_DOM {sectionRenderer(context, helpers)}:

    The content between the starting and ending tag. For example, Hello <b>World</b> is the LIGHT_DOM in the following:

    <my-tag>Hello <b>World</b></my-tag>
    

    The LIGHT_DOM can be positioned with a component’s view with the element. The data accessible to the LIGHT_DOM can be controlled with leakScope.

new Component([options])

Create an instance of a component without rendering it in a template. This is useful when you:

  • have complex logic for switching between different components (e.g. routing)
  • want to create components without adding them to the page (e.g. testing)

The following defines a MyGreeting component and creates a my-greeting element by calling new on the component’s constructor function:

const HelloWorld = Component.extend({
    tag: "hello-world",
    view: `
        <can-slot name="greetingTemplate" />
        <content>world</content>
        <ul>{{#each(items)}} {{this}} {{/each}}</ul>
    `,
    ViewModel: {
        items: {}
    }
});

// Create a new instance of our component
const componentInstance = new HelloWorld({

    // values with which to initialize the component’s view model
    viewModel: {
        items: ["eat"]
    },

    // can-stache template to replace any <content> elements in the component’s view
    content: "<em>{{message}}</em>",

    // <can-template> strings rendered by can-stache with the scope
    templates: {
        greetingTemplate: "{{greeting}}"
    },

    // scope with which to render the <content> and templates
    scope: {
        greeting: "Hello",
        message: "friend"
    }
});

myGreetingInstance.element; // is like <my-greeting>Hello <em>friend</em> <ul> <li>eat</li> </ul></my-greeting>

myGreetingInstance.viewModel; // is HelloWorld.ViewModel{items: ["eat"]}

Changing the component’s view model will cause its element and any bindings to be updated:

myGreetingInstance.viewModel.items.push("sleep");

myGreetingInstance.element; // is like <my-greeting>Hello <em>friend</em> <ul> <li>eat</li> <li>sleep</li> </ul></my-greeting>

See the Programmatically instantiating a component section for details.

Parameters

  1. options {Object}:

    Options for rendering the component, including:

    • content {String|Function}: Similar to the tag, the LIGHT_DOM to be rendered between the component’s starting and ending tags; can either be a string (which will be parsed by can-stache by default) or a renderer function.
    • scope {Object}: An object that is the scope with which the content should be rendered.
    • templates {Object<String,String|Function>}: An object that has keys that are <can-template> names and values that are either plain strings (parsed by can-stache by default) or renderer functions.
    • viewModel {Object}: An object with values to bind to the component’s view model.

Use

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

import Component from "can-component";

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

This element says “Click me” until a user clicks it and then says “Hello There!”. To create an instance of this component on the page, add <hello-world/> to a can-stache view, render the view, and insert the result in the page like:

import stache from "can-stache";

const renderer = stache( "<hello-world/>" );
document.body.appendChild( renderer( { } ) );

Check this out here:

Typically, you do not append a single component at a time. Instead, you’ll render a view with many custom tags like:

<srchr-app>
    <srchr-search models:from="models">
        <input name="search"/>
    </srchr-search>
    <ui-panel>
        <srchr-history/>
        <srchr-results models:from="models"/>
    </ui-panel>
</srchr-app>

You can also create an instance of a component without rendering it in the page. See the Programmatically instantiating a component section for details.

Defining a Component

Use extend to define a Component constructor function that automatically gets 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 view and pass any needed viewModel properties via attributes.

Tag

A component’s tag is the element node name that the component will be created on.

The following matches <hello-world> elements.

Component.extend( {
    tag: "hello-world"
} );

View

A component’s view is a template that is rendered as the element’s innerHTML.

The following component:

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

Changes <hello-world/> elements into:

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

Use the tag to position the custom element’s source HTML.

The following component:

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

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

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

ViewModel

A component’s ViewModel defines a constructor that creates instances used to render the component’s view. The instance’s properties are typically set by attribute data bindings on the custom element. By default, every data binding’s value is looked up in the parent can-view-scope of the custom element and added to the viewModel object.

The following component:

Component.extend( {
    tag: "hello-world",
    view: "<h1>{{message}}</h1>"
} );

Changes the following rendered view:

const renderer = stache( "<hello-world message:from='greeting'/>" );
renderer( {
    greeting: "Salutations"
} );

Into:

<hello-world><h1>Salutations</h1></hello-world>

Default values can be provided. The following component:

Component.extend( {
    tag: "hello-world",
    view: "<h1>{{message}}</h1>",
    viewModel: {
        message: "Hi"
    }
} );

Changes the following rendered view:

const renderer = stache( "<hello-world/>" );
renderer( {} );

Into:

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

If you want to set the string value of the attribute on the ViewModel, set an attribute without any binding syntax.

The following view, with the previous hello-world component:

const renderer = stache( "<hello-world message='Howdy'/>" );
renderer( {} );

Renders:

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

Lifecycle Hooks

Mainly used to set up special bindings, connectedCallback is defined on the viewModel and is called automatically when a component is inserted into the DOM. When writing tests, since connectedCallback is on the viewModel, it can be called manually to reduce complexity of tests that would otherwise need the full component to be initialized and inserted into the/a DOM. For that reason, connectedCallback is preferred to using inserted in events.

The following example listens to changes on the name property and counts them in the nameChanged property:

const Person = DefineMap.extend( {
    nameChanged: "number",
    name: "string",
    connectedCallback() {
        this.listenTo( "name", function() {
            this.nameChanged++;
        } );
        const disconnectedCallback = this.stopListening.bind( this );
        return disconnectedCallback;
    }
} );

The connectedCallback function may return a disconnectedCallback function this is called during teardown. Defined in the same closure scope as setup, its primary use is to tear down anything that was set up during the connectedCallback lifecycle hook.

Special bindings are used to set up observable property behaviors that are unable to be represented easily within the declarative APIs of the viewModel. It doesn’t remove all imperative code but will help keep imperative code isolated and leave other properties more testable. Otherwise, properties like name in the example above, would need side-effects in setters or getters:

const Person = DefineMap.extend( {
    nameChanged: "number",
    name: {
        type: "string",
        set: function( newVal, lastSetVal ) {
            this.nameChanged = ( this.nameChanged || 0 ) + 1;
            return newVal;
        }
    }
} );

This might look preferable but this pattern should be avoided. A more complex example would have side-effects changing a property (like nameChanged is in the name setter) coming from several different getters, setters, and methods all updating a common property. This makes debugging and testing each property more difficult.

There are additional ways to achieve the behavior, the most common are listed here in order of least preferable to most preferable:

side-effects < vm event bindings < listenTo in connectedCallback < streams

connectedCallback is named as such to match the web components spec for the same concept.

Events

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:

Component.extend( {
    tag: "hello-world",
    view: "<h1>{{message}}</h1>",
    events: {
        "click": function() {
            const currentMessage = this.viewModel.message;
            this.viewModel.message = currentMessage + "!";
        }
    }
} );

Use connectedCallback to listen to when an component’s element is inserted or removed from the DOM.

Helpers

A component’s helpers object provides stache helper functions that are available within the component’s view. The following component only renders friendly messages:

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

Generally speaking, helpers should only be used for view related functionality, like formatting a date. Data related methods should be in the view model or models.

Programmatically instantiating a component

You can also instantiate new component instances programmatically by using the component’s constructor function. This is useful when you:

  • have complex logic for switching between different components (e.g. routing)
  • want to create components without adding them to the page (e.g. testing)

The following defines a MyGreeting component and creates a my-greeting element by calling new on the component’s constructor function:

import Component from "can-component";

const MyGreeting = Component.extend({
  tag: "my-greeting",
  view: "Hello {{subject}}",
  ViewModel: {
    subject: "string"
  }
});

const myGreetingInstance = new MyGreeting({
  viewModel: {
    subject: "friend"
  }
});

myGreetingInstance.element; // is <my-greeting>Hello friend</my-greeting>

myGreetingInstance.viewModel; // is MyGreeting.ViewModel{subject: "friend"}

In the example above, the viewModel is passed in as an option to the component’s constructor function.

In addition to viewModel, there are templates, scope, and content options. Read below for details on all the options.

viewModel

The viewModel option is used to create the component’s view model and bind to it. For example:

import Component from "can-component";
import DefineMap from "can-define/map/map";
import value from "can-value";

const appVM = new DefineMap({
  association: "friend"
});

const MyGreeting = Component.extend({
  tag: "my-greeting",
  view: "{{greeting}} {{subject}}",
  ViewModel: {
    greeting: "string",
    subject: "string"
  }
});

const myGreetingInstance = new MyGreeting({
  viewModel: {
    greeting: "Hello",
    subject: value.bind(appVM, "association")
  }
});

myGreetingInstance.element; // is <my-greeting>Hello friend</my-greeting>

myGreetingInstance.viewModel; // is MyGreeting.ViewModel{subject: "friend"}

The way the component is instantiated above is similar to this example below, assuming it’s rendered by can-stache with appVM as the current scope:

<my-greeting greeting:raw="Hello" subject:bind="association"></my-greeting>

You can recreate one-way and two-way bindings with can-value, which has bind, from, and to methods for creating two-way, one-way parent-to-child, and one-way child-to-parent bindings, respectively.

const appVM = new DefineMap({
  family: {
    first: "Milo",
    last: "Flanders"
  }
});

const NameComponent = Component.extend({
  tag: "name-component",
  view: "{{fullName}}",
  ViewModel: {
    givenName: "string",
    familyName: "string",
    get fullName() {
      return this.givenName + " " + this.familyName;
    }
  }
});

const componentInstance = new NameComponent({
  viewModel: {
    givenName: value.from(appVM, "family.first"),
    familyName: value.bind(appVM, "family.last"),
    fullName: value.to(appVM, "family.full"),
  }
});

The way the component is instantiated above is similar to this example below, assuming it’s rendered by can-stache with appVM as the current scope:

<my-greeting
  givenName:from="family.first"
  familyName:bind="family.last"
  fullName:to="family.full"
></my-greeting>

This will result in an appVM with the following data:

{
  family: {
    first: "Milo",
    full: "Milo Flanders",
    last: "Flanders"
  }
}

Changing the component’s view model will cause its element and any bindings to be updated:

componentInstance.viewModel.familyName = "Smith";

componentInstance.element; // is <name-component>Milo Smith</name-component>

appVM.family.last; // is "Smith"

content

The content option is used to pass LIGHT_DOM into a component when it is instantiated, similar to the tag.

import Component from "can-component";

const HelloWorld = Component.extend({
  tag: "hello-world",
  view: "Hello <content>world</content>"
});

const helloWorldInstance = new HelloWorld({
  content: "<em>mundo</em>"
});

This would make helloWorldInstance.element a fragment with the following structure:

<hello-world>Hello <em>mundo</em></hello-world>

scope

You can also provide a scope with which the content should be rendered:

import Component from "can-component";

const HelloWorld = Component.extend({
  tag: "hello-world",
  view: "Hello <content>world</content>"
});

const helloWorldInstance = new HelloWorld({
  content: "<em>{{message}}</em>",
  scope: {
    message: "mundo"
  }
});

This would make helloWorldInstance.element a fragment with the following structure:

<hello-world>Hello <em>mundo</em></hello-world>

templates

The templates option is used to pass a into a component when it is instantiated.

import Component from "can-component";

const TodosPage = Component.extend({
    tag: "todos-page",
    view: "<ul><can-slot name='itemList' /></ul>"
});

const todosPageInstance = new TodosPage({
    scope: {
        items: ["eat"]
    },
    templates: {
        itemList: "{{#each(items)}} <li>{{this}}</li> {{/each}}"
    }
});

This would make todosPageInstance.element a fragment with the following structure:

<todos-page>
  <ul>
    <li>eat</li>
  </ul>
</todos-page>

Examples

Check out the following examples built with Component.

Tabs

The following demos a tabs widget. Click “Add Vegetables” to add a new tab.

An instance of the tabs widget is created by creating <my-tabs> and <my-panel> elements like:

<my-tabs>
    {{#each(foodTypes)}}
        <my-panel title:from="title">{{content}}</my-panel>
    {{/each}}
</my-tabs>

To add another panel, all we have to do is add data to foodTypes like:

foodTypes.push( {
    title: "Vegetables",
    content: "Carrots, peas, kale"
} );

The secret is that the <my-panel> element listens to when it is inserted and adds its data to the tabs’ list of panels with:

const vm = this.parentViewModel = canViewModel( this.element.parentNode );
vm.addPanel( this.viewModel );

TreeCombo

The following tree combo lets people walk through a hierarchy and select locations.

The secret to this widget is the viewModel’s breadcrumb property, which is an array of items the user has navigated through, and selectableItems, which represents the children of the last item in the breadcrumb. These are defined on the viewModel like:

DefineMap.extend( {
    breadcrumb: {
        Value: DefineList
    },
    selectableItems: {
        get: function() {
            const breadcrumb = this.breadcrumb;

            // if there’s an item in the breadcrumb
            if ( breadcrumb.length ) {

                // return the last item’s children
                const i = breadcrumb.length - 1;
                return breadcrumb[ i ].children;
            } else {

                // return the top list of items
                return this.items;
            }
        }
    }
} );

When the “+” icon is clicked next to each item, the viewModel’s showChildren method is called, which adds that item to the breadcrumb like:

DefineMap.extend( {
    showChildren: function( item, ev ) {
        ev.stopPropagation();
        this.breadcrumb.push( item );
    }
} );

Paginate

The following example shows 3 widget-like components: a grid, next / prev buttons, and a page count indicator. And, it shows an application component that puts them all together.

This demo uses a Paginate can-define/map/map to assist with maintaining a paginated state:

const Paginate = DefineMap.extend( {

    // ...
} );

The app component, using can-define/map/map, creates an instance of the Paginate model and a websitesPromise that represents a request for the Websites that should be displayed. Notice how the websitesCount value is updated when the websitesPromise resolves. connectedCallback is used to listen for changes to websitesCount, which then updates the paginate’s count value.

const AppViewModel = DefineMap.extend( {
    connectedCallback: function() {
        this.listenTo( "websitesCount", function( event, count ) {
            this.paginate.count = count;
        } );
        return this.stopListening.bind( this );
    },
    paginate: {
        default: function() {
            return new Paginate( {
                limit: 5
            } );
        }
    },
    websitesCount: {
        get: function( lastValue, setValue ) {
            this.websitesPromise.then( function( websites ) {
                setValue( websites.count );
            } );
        }
    },
    websitesPromise: {
        get: function() {
            return Website.getList( {
                limit: this.paginate.limit,
                offset: this.paginate.offset
            } );
        }
    }
} );

The my-app component passes paginate, paginate’s values, and websitesPromise to its sub-components:

<my-app>
    <my-grid promiseData:from="websitesPromise">
        {{#each(items)}}
            <tr>
                <td width="40%">{{name}}</td>
                <td width="70%">{{url}}</td>
            </tr>
        {{/each}}
    </my-grid>
    <next-prev paginate:from="paginate"></next-prev>
    <page-count page:from="paginate.page" count:from="paginate.pageCount"></page-count>
</my-app>

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 4.3.0.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news