can-component
Create a custom element that can be used to manage widgets or application logic.
Component
can-component
exports a Component
Construct constructor function used to
define custom elements.
Call Component.extend to define a custom element. Components are extended with a:
- tag - The custom element tag name.
- ViewModel - The methods and properties that manage the logic of the component. This is usually a DefineMap class.
- view - A template that writes the the inner HTML of
the custom element given the
ViewModel
. This is usually a can-stache template.
The following defines a <my-counter>
element:
const MyCounter = Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: {
count: {default: 0},
increment() {
this.count++;
}
}
});
To create a component instance, either:
- Write the element tag and bindings in a can-stache template like:
<my-counter count:from="5"/>
- Write the component tag in an HTML page and it will be mounted automatically:
<my-counter></my-counter>
- Create a new Component programatically like:
var myCounter = new MyCounter({ viewModel: { count: 6 } }); myCounter.element //-> <my-counter> myCounter.viewModel //-> MyCounterVM{count:6}
Purpose
Component
is used to define custom elements. Those custom elements are
used for many different layers within your application:
- Application Component - A component that houses global state, for example route data and
session data, and selects different pages
based upon the url, session and other information. Example:
<my-app>
- Page Component - Components that contain the functionality for a page. Example:
<todos-page>
- Functional Components - Component that provide functionality for a segment of a page. Example:
<todos-list>
,<todos-create>
- Widget/UI Components - Components that create controls that could be used many places. Example:
<ui-slider>
,<ui-tabs>
Component
is designed to be:
Testable - Components separate their logic into independently testable view and ViewModel pieces.
Flexible - There are many ways to manage logic in a component. Components can be:
- dumb - Get passed their data and can only call functions passed to them to change state.
- smart - Manage their own state and request data.
Components can also:
- Access their DOM element through connectedCallback. This is an escape hatch when the view is unable to update the DOM in a way you need.
- Support alternate ViewModels types like can-observe.
A bridge to web components - In browsers that support custom elements, extend will create a custom element. We've also adopted many custom element conventions such as:
- connectedCallback - Component's connectedCallback lifecycle hook
- slots - <can-slot>
- template - <can-slot>
- content - <content> (now obsolete)
Overview
On a high level using Component
is consists of two steps:
Extend
Component
with a tag, view and ViewModel to create a custom element:Component.extend({ tag: "my-counter", view: ` Count: <span>{{this.count}}</span> <button on:click="this.increment()">+1</button> `, ViewModel: { count: {default: 0}, increment() { this.count++; } } });
Use that element in your HTML or within another
Component
's view and use can-stache-bindings to pass values into or out of your component:<my-counter count:from="1"/>
The following video walks through how this component works:
Learning how to use Component
This section gives an overview of how to learn Component
. As Component
is a combination of many other technologies, many of its parts are
documented in detail on other pages.
Begin learning Component
by reading the HTML Guide to get a background on Component
and other related CanJS technology.
Learning Component
mostly means learning:
- DefineMap which serves as Component's ViewModels. A Component's ViewModel manages the logic and state of the component.
- stache which serves as Component's views. A Component's view translates the ViewModel into HTML to display to the user.
- stacheBindings which enable event binding and value passing between components and values in can-stache templates.
The following are good resources to learn these parts:
DefineMap
Read the Logic Guide on how to:
- Organize ViewModel properties
- Derive properties from other properties
- Update the DOM when stache is unable to cause the change
Checkout the Testing Guide on how to test ViewModels and components.
stache
Read stache's documentation on how to:
- Turn
ViewModel
values into HTML - Read promises
- Animate HTML
stacheBindings
Read stacheBindings documentation on how to:
- Listen to events on elements or components and call a function.
- Pass values between ViewModels and elements.
The forms guide details how to work with forms and form elements.
After the basics
Once you've got a good understanding of how to write a ViewModel, a view and pass values between them, this page is a good resource on how to do everything else with Component.
For a summary of all of CanJS's core APIs, checkout the API page.
Basic Use
The following sections cover:
- Defining a component with a tag, view and ViewModel.
- Creating a component in one of the following ways:
- In a stache template
- Directly in your HTML page
- Programatically with the component's constructor
- Component's lifecycle hooks
Defining a Component
Use extend to define a Component
constructor function
that automatically gets initialized whenever the component’s tag is
found.
import {Component} from "can";
const MyCounter = Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: {
count: {default: 0},
increment() {
this.count++;
}
}
});
Defining a Component's tag
A component’s tag is the element node name that the component will be created on. The tag should be hyphenated as follows:
Component.extend( {
tag: "my-counter"
} );
The previous component matches <my-counter>
elements.
Defining a Component's view
A component’s view is a template that is rendered as the element’s innerHTML.
This is typically a can-stache template that is imported or passed as a string.
The following component:
Component.extend({
tag: "my-counter",
view: ` Count: <span>1</span> `,
});
Changes <my-counter></my-counter>
into:
<my-counter> Count: <span>1</span> </my-counter>
You can see by inspecting the DOM in the following example:
<my-counter></my-counter>
<script type="module">
import {Component} from "can";
Component.extend({
tag: "my-counter",
view: ` Count: <span>1</span> `,
});
</script>
The view
is optional. Read here what happens if it is omitted.
The view
can also render the <can-template> tags passed to a
component using <can-slot> tags. Read more about this in Customizing a component's view.
Defining a Component's ViewModel
A component’s ViewModel defines a constructor that creates
instances used to render the component’s view. The ViewModel
manages the logic
and state of a component.
The ViewModel
can be defined separately
from the component.
import {Component, DefineMap} from "can";
const MyCounterVM = DefineMap.extend("MyCounterVM",{
count: {default: 0},
increment() {
this.count++;
}
});
const MyCounter = Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: MyCounterVM
});
In the previous example, MyCounterVM
has state (the count
property) and logic
(the increment
method). We could create a MyCounterVM
instance ourselves,
read its state and call its methods as follows:
import {DefineMap} from "can";
const MyCounterVM = DefineMap.extend("MyCounterVM",{
count: {default: 0},
increment() {
this.count++;
}
});
var myCounterVM = new MyCounterVM();
console.log( myCounterVM.count ) //-> 0
myCounterVM.increment()
console.log( myCounterVM.count ) //-> 1
Typically, the ViewModel
is defined inline on the component, as an
object as follows:
import {Component, DefineMap} from "can";
const MyCounter = Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: {
count: {default: 0},
increment() {
this.count++;
}
}
});
You can access the ViewModel
created on the component constructor as follows:
import {Component, DefineMap} from "can";
const MyCounter = Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: {
count: {default: 0},
increment() {
this.count++;
}
}
});
var myCounterVM = new MyCounter.ViewModel();
console.log( myCounterVM.count ) //-> 0
myCounterVM.increment()
console.log( myCounterVM.count ) //-> 1
Creating a component in stache
Components are usually created in the stache template of another component's view.
For example, a <my-counter/>
element is created in the <my-app>
component's view:
<my-app></my-app>
<script type="module">
import {Component} from "can";
Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: {
count: {default: 0},
increment() {
this.count++;
}
}
});
Component.extend({
tag: "my-app",
view: `<my-counter/>`
});
</script>
In stache, components can be written as self closing (like <my-counter/>
)
or have a closing tag (like <my-counter></my-counter>
).
Data and event bindings can be added to components to communicate across
components. The following cross binds <my-app>
's number
with <my-counter>
's count
:
<my-app></my-app>
<script type="module">
import {Component} from "can";
Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: {
count: {default: 0},
increment() {
this.count++;
}
}
});
Component.extend({
tag: "my-app",
view: `
Your Number is {{this.number}}.<br/>
<my-counter count:bind="this.number"/>
`,
ViewModel: {
number: {default: 4}
}
});
</script>
Read the <tag bindings...> docs for more information on what's available when creating components in stache, including:
- The bindings available.
- Passing <can-template> elements.
Creating a component in HTML
Component elements can also be inserted directly in the page. For
example, the following creates two <my-counter>
elements in the page:
<p><my-counter></my-counter></p>
<p><my-counter></my-counter></p>
<script type="module">
import {Component} from "can";
Component.extend({
tag: "my-counter",
view: `
Count: <span>{{this.count}}</span>
<button on:click="this.increment()">+1</button>
`,
ViewModel: {
count: {default: 0},
increment() {
this.count++;
}
}
});
</script>
Compared to components created in stache
, components created directly in HTML have a number of restrictions that are enumerated
here.
Creating a component programmatically
Component.extend returns a constructor function. These are often used for testing and Routing. It's used for dynamically selecting a component in the Multiple Modals recipe.
The following dynamically switches between two components:
<my-app></my-app>
<script type="module">
import {Component} from "can";
const GreenLight = Component.extend({
tag: "green-light",
view: `💚`
});
const RedLight = Component.extend({
tag: "red-light",
view: `❤️`
});
Component.extend({
tag: "my-app",
view: `
<button on:click="this.color = 'red'">Red</button>
<button on:click="this.color = 'green'">Green</button>
Color: {{component}}.
`,
ViewModel: {
green: {
default: () => new GreenLight()
},
red: {
default: () => new RedLight()
},
color: {default: "green"},
get component(){
if(this.color === "green") {
return this.green;
} else {
return this.red;
}
}
}
});
</script>
Read more about how to programmatically create components on the new Component page.
Lifecycle / Timing
Components have a lifecycle of method calls that you can hook into to perform various setup and teardown actions.
The following <lifecycle-component>
component logs the timing of various activities. The <my-app>
component
will add and remove <lifecycle-component>
from the page so you can see
when the bindings are called. Also, <lifecycle-component>
's childProperty
is two-way bound to <my-app>
's parentProperty
.
<my-app></my-app>
<script type="module">
import {Component, stache, DefineMap} from "can";
var view = stache("Added Lifecycle Component");
Component.extend({
tag: "lifecycle-component",
view: function(){
console.log("before the view is rendered");
var fragment = view.apply(this, arguments);
console.log("after the view is rendered");
return fragment;
},
ViewModel: {
setup: function(props){
console.log("before properties are set on the ViewModel");
return DefineMap.prototype.setup.apply(this, arguments);
},
init: function(){
console.log("after initial properties are set on the ViewModel");
},
connectedCallback(element) {
console.log("after the element is inserted in the page");
return () => {
console.log("after the element is removed from the page");
this.stopListening();
};
},
childProperty: {
value( {resolve} ){
console.log("childProperty bound and read");
resolve("childProperty");
return function(){
console.log("childProperty unbound");
}
}
}
}
});
Component.extend({
tag: "my-app",
view: `
<button on:click="this.toggle()">
{{# if(this.show) }} Remove {{else}} Add {{/}} Component
</button>
{{# if(this.show) }}
<lifecycle-component childProperty:bind="this.parentProperty"/>
{{else}}
Removed Lifecycle Component
{{/}}
`,
ViewModel: {
show: {default: false},
toggle(){
this.show = !this.show;
},
parentProperty: {
value( {resolve} ){
console.log("parentProperty bound and read");
resolve("parentProperty");
return function(){
console.log("parentProperty unbound");
}
}
}
}
})
</script>
When <lifecycle-component>
is added to the page, the following will be logged:
- parentProperty bound and read - When a component is created, we will initialize the
ViewModel
with component bindings (key:from or key:bind) that read from the scope. Before anything else happens, the right hand side of bindings likechildProperty:bind="this.parentProperty"
will be bound and read to be prepared to initialize a newViewModel
. - before properties are set on the ViewModel - As DefineMap inherits from
Construct, you can overwrite initialization behavior in setup
and init. DefineMap's
setup
function will set all properties on theViewModel
.setup
can use ReturnValue to return alternative instances. - after initial properties are set on the ViewModel - Once all initial properties are set on
the
ViewModel
, theViewModel
's init method will be called. This can be a good time to make sure the ViewModel is ready for being passed to the view. - childProperty bound and read - Once the
ViewModel
is created, any component bindings that read the ViewModel will be bound and (key:to or key:bind) read. At this time, the parent binding value might be updated. - before the view is rendered - The component's
view
function will be called with theViewModel
. - after the view is rendered - After the
view
is rendered, the result will be a document fragment that is not attached to the page. - after the element is inserted in the page - The document fragment has been inserted within the component element and the component element has been inserted into the document. This is a good place to setup any stateful side effects as shown in the Logic guide or read the DOM.
When <lifecycle-component>
is removed from the page, the following will be logged:
- parentProperty unbound - When the element is removed, its bindings are town down immediately, starting with the parent value of a binding.
- childProperty unbound - Next, the child value of the parent is town down.
- after the element is removed from the page - Finally the
disconnectedCallback
is called.
Other Uses
The following shows (or links to) how to solve common use cases.
Customizing a component's layout
Often, you want to allow consumers of a component to adjust the HTML of a component. There are two ways of doing this:
- Using
<can-slot>
and<can-template>
. - Passing a
view
Slots and templates
<can-template> Allows you to pass a view to a component's view where its content can be inserted with a corresponding <can-slot> as follows:
<my-app></my-app>
<script type="module">
import {Component} from "can";
Component.extend({
tag: "hello-world",
view: `
{{# if(this.show) }}
<can-slot name="helloGreeting"
message:from="this.message"/>
{{/ if }}
`,
ViewModel: {
show: {
value({resolve}){
var show = resolve(true);
var interval = setInterval( () => resolve(show = !show), 1000);
return () => clearInterval(interval);
}
},
message: {default: "world"}
}
});
Component.extend({
tag: "my-app",
view: `
<hello-world>
<can-template name="helloGreeting">
<h1>Hello {{message}}!</h1>
</can-template>
</hello-world>
`
})
</script>
Read <can-slot>'s documentation for more information on this technique.
Passing a view
There are two common ways of creating an passing a view:
- Creating an inline partial.
- Creating a view programmatically as a property value.
The following creates a helloGreeting
inline partial and passes it to
<hello-world>
to be rendered.
<my-app></my-app>
<script type="module">
import {Component} from "can";
Component.extend({
tag: "hello-world",
view: `
{{# if(this.show) }}
{{ greeting(this) }}
{{/ if }}
`,
ViewModel: {
show: {
value({resolve}){
var show = resolve(true);
var interval = setInterval( () => resolve(show = !show), 1000);
return () => clearInterval(interval);
}
},
greeting: "any",
message: {default: "world"}
}
});
Component.extend({
tag: "my-app",
view: `
{{<helloGreeting}}
<h1>Hello {{message}}!</h1>
{{/helloGreeting}}
<hello-world greeting:from="helloGreeting"/>
`
})
</script>
The following does the same thing, but creates this.helloGreeting
as a ViewModel property:
<my-app></my-app>
<script type="module">
import {Component, stache} from "can";
Component.extend({
tag: "hello-world",
view: `
{{# if(this.show) }}
{{ greeting(this) }}
{{/ if }}
`,
ViewModel: {
show: {
value({resolve}){
var show = resolve(true);
var interval = setInterval( () => resolve(show = !show), 1000);
return () => clearInterval(interval);
}
},
greeting: "any",
message: {default: "world"}
}
});
Component.extend({
tag: "my-app",
view: `
<hello-world greeting:from="this.helloGreeting"/>
`,
ViewModel: {
helloGreeting: {
default: ()=> stache(`<h1>Hello {{message}}!</h1>`)
}
}
})
</script>
Debugging Components
Read the Debugging guide to learn how to solve common issues with components.
Inheriting Components
Read the extend docs on inheriting for how to inherit from a base component.
Manipulating or reading the DOM outside the view
The stache view should be how your component interacts with the DOM the vast majority of the time. However, sometimes it's not able to do everything you need. In these circumstances you should use connectedCallback to get the component's element and do what you need.
The Slider example shows using connectedCallback to update a
component's width
property when the page is resized.
connectedCallback(el) {
// derive the width
this.width = width(el) - el.firstElementChild.offsetWidth;
this.listenTo(window,"resize", () => {
this.width = width(el) - el.firstElementChild.offsetWidth;
});
...
}
The Video Player recipe shows calling .play()
or .pause()
on
a <video>
element when the component's playing
state changes:
connectedCallback(element) {
this.listenTo("playing", function(event, isPlaying) {
if (isPlaying) {
element.querySelector("video").play();
} else {
element.querySelector("video").pause();
}
});
...
}
Examples
Check out the following examples built with Component
.
Slider
The following creates a draggable slider. It uses connectedCallback
to update the component's width
when the page is resized.
<percent-slider value:from="50"></percent-slider>
<script type="module">
import { Component } from "can";
function width(el) {
var cs = window.getComputedStyle(el,null)
return el.clientWidth - parseFloat( cs.getPropertyValue("padding-left") )
- parseFloat( cs.getPropertyValue("padding-left") );
}
Component.extend({
tag: "percent-slider",
view: `
<div class='slider'
style="left: {{ left }}px"
on:mousedown='startDrag(scope.event.clientX)'/>`,
ViewModel: {
start: {type: "number", default: 0},
end: {type: "number", default: 100},
currentValue: {
default: function(){
return this.value || 0;
}
},
width: {type: "number", default: 0},
get left(){
var left = this.currentValue / this.end * this.width;
return Math.min( Math.max(0, left), this.width) || 0;
},
connectedCallback(el) {
// derive the width
this.width = width(el) - el.firstElementChild.offsetWidth;
this.listenTo(window,"resize", () => {
this.width = width(el) - el.firstElementChild.offsetWidth;
});
// Produce dragmove and dragup events on the view-model
this.listenTo("startClientX", () => {
var startLeft = this.left;
this.listenTo(document,"mousemove", (event)=>{
this.dispatch("dragmove", [event.clientX - this.startClientX + startLeft]);
});
this.listenTo(document,"mouseup", (event)=>{
this.dispatch("dragup", [event.clientX - this.startClientX + startLeft]);
this.stopListening(document);
})
});
// Update the slider position when currentValue changes
this.listenTo("dragmove", (ev, left)=> {
this.currentValue = (left / this.width) * (this.end - this.start);
},"notify");
// If the value is set, update the current value
this.listenTo("value", (ev, newValue) => {
this.currentValue = newValue;
}, "notify");
// Update the value on a dragmove
this.listenTo("dragup", (ev, left)=> {
this.value = (left / this.width) * (this.end - this.start);
},"notify");
return this.stopListening.bind(this);
},
startClientX: "any",
startDrag(clientX) {
this.startClientX = clientX;
}
}
});
</script>
<style>
.slider {
border: solid 1px blue;
background-color: red;
height: 40px;
width: 40px;
cursor: ew-resize;
position: relative;
}
percent-slider {
border: solid 4px black;
padding: 5px;
display: block;
}
</style>
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: {
Default: 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>