can-component
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
- TAG
{String}
:An HTML tag name that matches the tag property of the component. Tag names should include a hypen (
-
) or a colon (:
) like:acme-tabs
oracme:tabs
. - 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
Note that because DOM attribute names are case-insensitive, use hypens (
-
) in the attribute name to setup forcamelCase
properties.Example:
<my-tag getChild:from="expression" setParent:to="expression" twoWay:bind="expression" on:event="callExpression()"></my-tag>
- 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 aname
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>
- LIGHT_DOM
{sectionRenderer(context, helpers)}
:The content between the starting and ending tag. For example,
Hello <b>World</b>
is theLIGHT_DOM
in the following:<my-tag>Hello <b>World</b></my-tag>
The
LIGHT_DOM
can be positioned with a component’s view with theelement. The data accessible to the LIGHT_DOM
can be controlled with leakScope.
Use
To create a Component, you must first extend Component
with the methods and properties of how your component behaves:
var Component = require("can-component");
var DefineMap = require("can-define/map/map");
var stache = require("can-stache");
var HelloWorldVM = DefineMap.extend({
visible: {value: false},
message: {value: "Hello There!"}
});
Component.extend({
tag: "hello-world",
view: stache("{{#if visible}}{{message}}{{else}}Click me{{/if}}"),
ViewModel: HelloWorldVM,
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:
var 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>
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: stache("<h1>Hello World</h1>")
});
Changes <hello-world/>
elements into:
<hello-world><h1>Hello World</h1></hello-world>
Use the
The following component:
Component.extend({
tag: "hello-world",
view: stache("<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: stache("<h1>{{message}}</h1>")
});
Changes the following rendered view:
var 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: stache("<h1>{{message}}</h1>"),
viewModel: {
message: "Hi"
}
});
Changes the following rendered view:
var 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:
var renderer = stache("<hello-world message='Howdy'/>");
renderer({});
Renders:
<hello-world><h1>Howdy</h1></hello-world>
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: stache("<h1>{{message}}</h1>"),
events: {
"click" : function(){
var currentMessage = this.viewModel.message;
this.viewModel.message = currentMessage+ "!";
}
}
});
Components have the ability to bind to special inserted, beforeremove and removed events that are called when a component’s tag has been inserted into, is about to removed, or was removed from the page.
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: 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();
}
}
}
});
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.
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:
var 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:
breadcrumb: {
Value: DefineList
},
selectableItems: {
get: function(){
var breadcrumb = this.breadcrumb;
// if there's an item in the breadcrumb
if(breadcrumb.length){
// return the last item's children
var 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:
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:
var 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 paginate’s count
value is tied to the
value of the websitesPromise
’s resolved value
’s count
.
var AppViewModel = DefineMap.extend({
paginate: {
value: function() {
return new Paginate({
limit: 5,
count: compute(this, "websitesPromise.value.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>