UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

443 lines (312 loc) 13.1 kB
@constructor can.Component @download can/component @test can/component/test.html @parent canjs @release 2.0 @link ../docco/component/component.html docco @description Create widgets that use a template, a view-model and custom tags. @signature `< TAG BINDINGS... >` Create an instance of a component on a particular tag in a [can.stache] template. In 2.3, use the [can.view.bindings bindings] syntaxes to setup bindings. @release 2.3 @param {String} TAG An HTML tag name that matches the [can.Component::tag tag] property of the component. @param {can.view.bindings} BINDINGS Use the following binding syntaxes to connect the component's [can.Component::viewModel] to the template's [can.view.Scope scope]: - [can.view.bindings.toChild]=[can.stache.key] - one way binding to child - [can.view.bindings.toParent]=[can.stache.key] - one way binding to parent - [can.view.bindings.twoWay]=[can.stache.key] - two way binding child to parent Example: ``` <my-tag {to-child}="key" {^to-parent}="key" {(two-way)}="key"></my-tag> ``` @signature `< TAG [ATTR-NAME="{KEY}|ATTR-VALUE"] >` Create an instance of a component on a particular tag in a [can.stache] template. This form of two way bindings is deprecated as of 2.3. It looked like: ``` <my-tag attr-name="{key}"></my-tag> ``` @release 2.1 Please check earlier versions of the documentation for more information. @signature `< TAG [ATTR-NAME=KEY|ATTR-VALUE] >` Create an instance of a component on a particular tag in a [can.mustache] template. Use of [can.mustache] is deprecated as of 2.3. Please check earlier versions of the documentation for more information. @body ## Use To create a `can.Component`, you must first [can.Component.extend 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") ); } } }); This element says "Click me" until a user clicks it and then says "Hello There!". To create a a instance of this component on the page, add `<hello-world></hello-world>` to a mustache template, render the template and insert the result in the page like: var template = can.stache("<hello-world></hello-world>"); $(document.body).append( template() ); Check this out here: @demo can/component/examples/click_me.html Typically, you do not append a single component at a time. Instead, you'll render a template with many custom tags like: <srchr-app> <srchr-search models="models"> <input name="search"/> </srchr-search> <ui-panel> <srchr-history/> <srchr-results models="models"/> </ui-panel> </srchr-app> ### Creating a can.Component 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. ### Tag A component's [can.Component::tag tag] is the element node name that the component will be created on. The following matches `<hello-world>` elements. can.Component.extend({ tag: "hello-world" }); ### Template A component's [can.Component::template template] is rendered as the element's innerHTML. The following component: can.Component.extend({ tag: "hello-world", template: can.stache("<h1>Hello World</h1>") }); Changes `<hello-world></hello-world>` elements into: <hello-world><h1>Hello World</h1></hello-world> 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> ### viewModel A component's [can.Component::viewModel viewModel] defines a can.Map that is used to render the component's template. The maps properties are typically set by attribute [can.view.bindings bindings] on the custom element. By default, every attribute's value is looked up in the parent viewModel of the custom element and added to the viewModel object. The following component: can.Component.extend({ tag: "hello-world", template: can.stache("<h1>{{message}}</h1>") }); Changes the following rendered template: var template = can.stache("<hello-world {message}='greeting'/>"); template({ greeting: "Salutations" }) Into: <hello-world><h1>Salutations</h1></hello-world> Default values can be provided. The following component: can.Component.extend({ tag: "hello-world", template: can.stache("<h1>{{message}}</h1>"), viewModel: { message: "Hi" } }); Changes the following rendered template: var template = can.stache("<hello-world/>"); template({}) Into: <hello-world><h1>Hi</h1></hello-world> 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({}) Renders to: <hello-world><h1>Howdy</h1></hello-world> ### Events A component's [can.Component::events events] object is used to listen to events (that are not listened to with [can.view.bindings 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+ "!") } } }); Components have the ability to bind to special [can.events.inserted inserted] and [can.events.removed removed] events that are called when a component's tag has been inserted into or removed from the page. ### Helpers A component's [can.Component::helpers helpers] object provides [can.mustache.helper mustache helper] functions that are available within the component's template. The following component only renders friendly messages: 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(); } } } }); ## Differences between components in can.mustache and can.stache A [can.mustache] template passes values from the viewModel to a `can.Component` by specifying the key of the value in the attribute directly. For example: can.Component.extend({ tag: "my-tag", template: "<h1>{{greeting}}</h1>" }); var template = can.mustache("<my-tag greeting='message'></my-tag>"); var frag = template({ message: "Hi" }); frag //-> <my-tag greeting='message'><h1>Hi</h1></my-tag> With [can.stache], you wrap the attribute name with `{}` for parent to child binding. For example: can.Component.extend({ tag: "my-tag", template: can.stache("<h1>{{greeting}}</h1>") }); var template = can.stache("<my-tag {greeting}='message'></my-tag>"); var frag = template({ message: "Hi" }); frag //-> <my-tag {greeting}='message'><h1>Hi</h1></my-tag> If the key was not wrapped, the template would render: frag //-> <my-tag greeting='message'><h1>message</h1></my-tag> Because the attribute value would be passed as the value of `greeting`. ## Examples Check out the following examples built with `can.Component`. ### Tabs The following demos a tabs widget. Click "Add Vegetables" to add a new tab. @demo can/component/examples/tabs.html An instance of the tabs widget is created by creating `<tabs>` and `<panel>` elements like: <tabs> {{#each foodTypes}} <panel title='title'>{{content}}</panel> {{/each}} </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 `<panel>` element listens to when it is inserted and adds its data to the tabs' list of panels with: this.element.parent().viewModel().addPanel( this.viewModel ); ### TreeCombo The following tree combo lets people walk through a hierarchy and select locations. @demo can/component/examples/treecombo.html 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 breadcrub. These are defined on the viewModel like: breadcrumb: [], selectableItems: function(){ var breadcrumb = this.attr("breadcrumb"); // if there's an item in the breadcrumb if(breadcrumb.attr('length')){ // return the last item's children return breadcrumb.attr(""+(breadcrumb.length-1)+'.children'); } else{ // return the top list of items return this.attr('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.attr('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. @demo can/component/examples/paginate.html This demo uses a `Paginate` can.Map to assist with maintaining a paginated state: var Paginate = can.Map.extend({ ... }); The `app` component, using the [can.Map.define define plugin], creates an instance of the `Paginate` model and a `websitesPromise` that represents a request for the Websites that should be displayed. viewModel: { define: { paginate: { value: function() { return new Paginate({ limit: 5 }); } }, websitesPromise: { get: function() { var params = { limit: this.attr('paginate.limit'), offset: this.attr('paginate.offset') }, websitesPromise = Website.findAll(params), self = this; websitesPromise.then(function(websites) { self.attr('paginate.count', websites.count); }); return websitesPromise; } } } } The `app` control passes paginate, paginate's values, and websitesPromise to its sub-components: <app> <grid {promise-data}='websitesPromise'> {{#each items}} <tr> <td width='40%'>{{name}}</td> <td width='70%'>{{url}}</td> </tr> {{/each}} </grid> <next-prev {paginate}='paginate'></next-prev> <page-count {page}='paginate.page' {count}='paginate.pageCount'/> </app> ## IE 8 Support 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](https://github.com/aFarkas/html5shiv) 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. ## Videos Watch this video for an overview of can.Component, why you should use it, and a hello world example: <iframe width="662" height="372" src="https://www.youtube.com/embed/BM1Jc3lVUrk" frameborder="0" allowfullscreen></iframe> This video provides a more in depth overview of the API and goes over several examples of can.Components: <iframe width="662" height="372" src="https://www.youtube.com/embed/ogX765S4iuc" frameborder="0" allowfullscreen></iframe> Note: the videos above reference the `scope` property, which was replaced by the [can.Component::viewModel viewModel] property in 2.2.