├── .npmignore ├── .gitignore ├── .travis.yml ├── test ├── index.js ├── hyperd.js └── component.js ├── index.js ├── examples ├── counter │ └── index.html └── counter-component │ └── index.html ├── lib ├── widget.js └── component.js ├── package.json ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .gitignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.12 4 | sudo: false 5 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('es5-shim'); 2 | require('./hyperd'); 3 | require('./component'); 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Component = require('./lib/component'); 2 | 3 | exports = module.exports = hyperd; 4 | 5 | exports.Component = Component; 6 | 7 | function hyperd(node, render) { 8 | var component = new Component(); 9 | component.render = render; 10 | return component.attachTo(node); 11 | } 12 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/counter-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/widget.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = Widget; 3 | 4 | function Widget(Component, properties) { 5 | this.Component = Component; 6 | this.properties = properties; 7 | this.component = null; 8 | this.rendered = false; 9 | this.key = this.properties.dataset ? this.properties.dataset.hkey : null; 10 | } 11 | 12 | Widget.prototype.type = 'Widget'; 13 | 14 | Widget.prototype.init = function() { 15 | this.component = new this.Component(this.properties); 16 | this.rendered = this.component.doRender(); 17 | return this.component.node; 18 | }; 19 | 20 | Widget.prototype.update = function(previous, domNode) { 21 | this.component = previous.component; 22 | this.component.setProps(this.properties); 23 | this.rendered = this.component.doRender(); 24 | }; 25 | 26 | Widget.prototype.destroy = function(domNode) { 27 | this.component.destroy(); 28 | }; 29 | -------------------------------------------------------------------------------- /test/hyperd.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var hyperd = require('../'); 3 | 4 | describe('hyperd', function() { 5 | beforeEach(function() { 6 | this.node = document.createElement('div'); 7 | document.body.appendChild(this.node); 8 | }); 9 | 10 | afterEach(function() { 11 | document.body.removeChild(this.node); 12 | }); 13 | 14 | it('should expose classes', function() { 15 | expect(hyperd.Component).to.be.a(Function); 16 | }); 17 | 18 | it('should create a component', function(done) { 19 | var component = hyperd(this.node, function() { 20 | return '
woot
'; 21 | }); 22 | component.on('render', function() { 23 | expect(this.node.innerHTML).to.be('woot'); 24 | component.destroy(); 25 | done(); 26 | }); 27 | 28 | expect(component).to.be.a(hyperd.Component); 29 | expect(component.node).to.be(this.node); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperd", 3 | "version": "0.1.0", 4 | "description": "Virtual DOM based, template engine agnostic, a lightweight view library", 5 | "keywords": [ 6 | "virtual", 7 | "dom", 8 | "component", 9 | "view" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/nkzawa/hyperd" 14 | }, 15 | "author": "Naoyuki Kanezawa ", 16 | "license": "MIT", 17 | "scripts": { 18 | "prepublish": "browserify -s hyperd index.js > hyperd.js", 19 | "test": "zuul --phantom --ui mocha-bdd -- test/index.js" 20 | }, 21 | "dependencies": { 22 | "clone": "1.0.2", 23 | "deep-equal": "1.0.0", 24 | "dom-delegate": "2.0.3", 25 | "html-to-vdom": "nkzawa/html-to-vdom", 26 | "raf": "2.0.4", 27 | "trim": "0.0.1", 28 | "virtual-dom": "2.0.1", 29 | "xtend": "4.0.0" 30 | }, 31 | "devDependencies": { 32 | "browserify": "~10.1.1", 33 | "es5-shim": "~4.1.1", 34 | "expect.js": "~0.3.1", 35 | "phantomjs": "~1.9.16", 36 | "zuul": "~3.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Naoyuki Kanezawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hyperd 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/nkzawa/hyperd.svg)](https://travis-ci.org/nkzawa/hyperd) 5 | 6 | Virtual DOM based, template engine agnostic UI library. 7 | 8 | ```js 9 | var component = hyperd(document.getElementById('count'), function() { 10 | return '
Counter: ' + this.data.count + '
'; 11 | }); 12 | 13 | component.data.count = 0; 14 | 15 | setInterval(function() { 16 | component.data.count++; 17 | }, 1000); 18 | ``` 19 | 20 | Differently from any other Virtual DOM based libraries, your UI is defined as just a html string on this library, which allows you to use along with your favorite template engines in a flexible manner. 21 | 22 | ## Installation 23 | 24 | $ npm install hyperd 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | #### With Bower 31 | 32 | $ bower install hyperd 33 | 34 | ## Features 35 | 36 | - Virtual DOM diffing 37 | - Template engine agnostic 38 | - Auto-redrawing 39 | - Building reusable components 40 | - Small API 41 | 42 | ## Examples 43 | 44 | - [Counter](http://nkzawa.github.io/hyperd/examples/counter) ([source](https://github.com/nkzawa/hyperd/tree/master/examples/counter/index.html)) 45 | - [Counter Component](http://nkzawa.github.io/hyperd/examples/counter-component) ([source](https://github.com/nkzawa/hyperd/tree/master/examples/counter-component/index.html)) 46 | - [TodoMVC](http://nkzawa.github.io/hyperd-todomvc/) ([source](https://github.com/nkzawa/hyperd-todomvc)) 47 | 48 | ## API Documentation 49 | 50 | #### hyperd(node, render) 51 | 52 | - `node` HTMLElement Node to attach 53 | - `render` Function Called upon redrawing, must return a html string. 54 | - Return: hyperd.Component 55 | 56 | A short circuit to create a component instance. 57 | 58 | ### Class: hyperd.Component 59 | 60 | The base component class. 61 | 62 | #### Class Method: hyperd.Component.extend(proto); 63 | 64 | - `proto` Object protoype object 65 | - Return: Function A new component constructor 66 | 67 | Creates a new component class. 68 | 69 | ```js 70 | var MyComponent = hyperd.Component.extend({ 71 | render: function() { 72 | return '
hi
'; 73 | } 74 | }); 75 | ``` 76 | 77 | #### new hyperd.Component([props]) 78 | 79 | - `props` Object properties 80 | 81 | In classes that extends `hyperd.Component`, make sure to call the super constructor so that the all settings can be initialized. 82 | 83 | ```js 84 | hyperd.Component.extend({ 85 | constructor: function(props) { 86 | hyperd.Component.apply(this, arguments); 87 | ... 88 | } 89 | }); 90 | ``` 91 | 92 | #### component.props 93 | 94 | The properties of the component. 95 | 96 | ```js 97 | var component = new MyComponent({ foo: "hi" }); 98 | console.log(component.props.foo); // "hi" 99 | ``` 100 | 101 | #### component.data 102 | 103 | The state data of the component. Mutating `data` triggers UI updates. 104 | 105 | #### component.node 106 | 107 | The node element which the component is attached to. 108 | 109 | #### component.components 110 | 111 | Definitions of child components. You can use defined components like a custom element on `render()`. 112 | 113 | ```js 114 | var Child = hyperd.Component.extend({ 115 | render: function() { 116 | return '
' + this.props.foo + '
'; 117 | } 118 | }); 119 | 120 | hyperd.Component.extend({ 121 | components: { child: Child }, 122 | render: function() { 123 | return '
' 124 | } 125 | }); 126 | ``` 127 | 128 | #### component.attachTo(node) 129 | 130 | - `node` HTMLElement 131 | - Return: `this` 132 | 133 | Attaches the component to a DOM node. 134 | 135 | ```js 136 | new MyComponent().attachTo(document.getElementById('foo')); 137 | ``` 138 | 139 | #### component.render() 140 | 141 | - Return: String A html string to render. 142 | 143 | Note: **implement this function, but do NOT call it directly**. 144 | 145 | Required to implement. This method is called automatically and asynchronously when you update `component.data`. 146 | 147 | #### component.destroy() 148 | 149 | Teardown and delete all properties and event bindings including descendant components. 150 | 151 | #### component.emit(event\[, args1\]\[, args2\]\[, ...\]) 152 | 153 | - `event` String The event type to be triggered. 154 | - Return: `this` 155 | 156 | Trigger a DOM event for `component.node` with the supplied arguments. 157 | 158 | ```js 159 | component.emit('edit', arg); 160 | ``` 161 | 162 | #### component.on(event, listener) 163 | 164 | - `event` String The event type. 165 | - `listener` Function 166 | - Return: `this` 167 | 168 | Add a listener for the specified event. 169 | 170 | ```js 171 | component.on('render', function() { ... }); 172 | ``` 173 | 174 | #### component.on(event, selector, listener) 175 | 176 | - `event` String The event type. 177 | - `selector` String CSS selector. 178 | - `listener` Function The listener always take an event object as the first argument. 179 | - Return: `this` 180 | 181 | Add a listener for the delegated event. The listener is called only for descendants that match the `selector`. You can use this to listen an event of a child component too. 182 | 183 | ```js 184 | hyperd.Component.extend({ 185 | constructor: function() { 186 | hyperd.Component.apply(this, arguments); 187 | this.on('click', 'button', function(event) { 188 | console.log('clicked!'); 189 | }); 190 | } 191 | render: function() { 192 | return '
' 193 | } 194 | }); 195 | ``` 196 | 197 | #### component.removeListener(event, listener) 198 | 199 | Remove a listener for the specified event. 200 | 201 | #### component.removeListener(event, selector, listener) 202 | 203 | Remove a listener for the delegated event. 204 | 205 | #### component.removeAllListeners(\[event\]\[, selector\]) 206 | 207 | Remove all listeners, or those of the specified event or the delegated event. 208 | 209 | #### component.onAttach() 210 | 211 | Called upon after the component is attached to a node. 212 | 213 | #### component.onRender() 214 | 215 | Called upon after the component is rerendered. 216 | 217 | #### component.onDestroy() 218 | 219 | Called upon after the component is destroyed. 220 | 221 | #### Event: 'attach' 222 | 223 | The same as `component.onAttach`. 224 | 225 | ```js 226 | component.on('attach', function() { ... }); 227 | ``` 228 | 229 | #### Event: 'render' 230 | 231 | The same as `component.onRender`. 232 | 233 | #### Event: 'destroy' 234 | 235 | The same as `component.onDestroy`. 236 | 237 | #### Attribute: data-hkey 238 | 239 | The identifier used to differentiate a node for Virtual DOM diffing. Used to reconcile an element will be reordered or destroyed. 240 | 241 | ```js 242 | hyperd.Component.extend({ 243 | render: function() { 244 | var items = ['foo', 'bar', 'baz']; 245 | return ''; 248 | } 249 | }); 250 | ``` 251 | 252 | ## Licence 253 | 254 | MIT 255 | -------------------------------------------------------------------------------- /test/component.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var hyperd = require('../'); 3 | 4 | describe('Component', function() { 5 | beforeEach(function() { 6 | this.node = document.createElement('div'); 7 | document.body.appendChild(this.node); 8 | }); 9 | 10 | afterEach(function() { 11 | document.body.removeChild(this.node); 12 | }); 13 | 14 | describe('#attachTo', function() { 15 | it('should attach to the node', function() { 16 | var Component = hyperd.Component.extend({ 17 | render: function() { return '
'; } 18 | }); 19 | var component = new Component().attachTo(this.node); 20 | expect(component.node).to.be(this.node); 21 | component.destroy(); 22 | }); 23 | }); 24 | 25 | describe('#render', function() { 26 | it('should create html text', function() { 27 | var Component = hyperd.Component.extend({ 28 | render: function() { 29 | return '
' + this.props.greeting + '
'; 30 | } 31 | }); 32 | var component = new Component({ greeting: 'hi' }); 33 | expect(component.render()).to.be('
hi
'); 34 | component.destroy(); 35 | }); 36 | }); 37 | 38 | describe('#emit', function() { 39 | it('should dispatch a custom dom event', function(done) { 40 | var Main = hyperd.Component.extend({ 41 | render: function() { 42 | return '
'; 43 | }, 44 | onRender: function() { 45 | this.emit('foo', 'hi'); 46 | } 47 | }); 48 | var App = hyperd.Component.extend({ 49 | components: { 50 | main: Main 51 | }, 52 | constructor: function() { 53 | hyperd.Component.apply(this, arguments); 54 | this.on('foo', '.main', function(e, v) { 55 | expect(v).to.eql('hi'); 56 | this.destroy(); 57 | done(); 58 | }); 59 | }, 60 | render: function() { 61 | return '
'; 62 | } 63 | }); 64 | new App().attachTo(this.node); 65 | }); 66 | }); 67 | 68 | describe('#on', function() { 69 | it('should listen a delegated event', function(done) { 70 | var Component = hyperd.Component.extend({ 71 | render: function() { 72 | return '
'; 73 | } 74 | }); 75 | var component = new Component().attachTo(this.node); 76 | component 77 | .on('click', 'button', function(e) { 78 | expect(this).to.be(component); 79 | component.destroy(); 80 | done(); 81 | }) 82 | .on('render', function() { 83 | this.node.querySelector('button').click(); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('#removeListener', function() { 89 | it('should not listen a delegated event', function(done) { 90 | var Component = hyperd.Component.extend({ 91 | render: function() { 92 | return '
'; 93 | } 94 | }); 95 | var component = new Component().attachTo(this.node); 96 | component 97 | .on('click', 'button', onclick) 98 | .on('render', function() { 99 | this.removeListener('click', 'button', onclick); 100 | this.node.querySelector('button').click(); 101 | // wait for a potential event call; 102 | setTimeout(function() { 103 | component.destroy(); 104 | done(); 105 | }, 100); 106 | }); 107 | 108 | function onclick() { 109 | expect().fail('Unexpectedly called'); 110 | } 111 | }); 112 | }); 113 | 114 | describe('#onAttach', function() { 115 | it('should be triggered upon attachTo', function(done) { 116 | var self = this; 117 | var called = false; 118 | var Component = hyperd.Component.extend({ 119 | render: function() { 120 | return '
'; 121 | }, 122 | onAttach: function() { 123 | expect(called).to.be.ok(); 124 | done(); 125 | } 126 | }); 127 | var component = new Component(); 128 | // wait for possible event call 129 | setTimeout(function() { 130 | called = true; 131 | component.attachTo(self.node); 132 | }, 500); 133 | }); 134 | }); 135 | 136 | it('should render html', function(done) { 137 | var Component = hyperd.Component.extend({ 138 | render: function() { 139 | return '
' + this.props.greeting + '
'; 140 | }, 141 | onRender: function() { 142 | expect(this.props.greeting).to.be('hi'); 143 | expect(this.node.innerHTML).to.be('hi') 144 | this.destroy(); 145 | done(); 146 | } 147 | }); 148 | new Component({ greeting: 'hi' }).attachTo(this.node); 149 | }); 150 | 151 | it('should re-render when data changed', function(done) { 152 | var Component = hyperd.Component.extend({ 153 | constructor: function() { 154 | hyperd.Component.apply(this, arguments); 155 | this.data.count = 0; 156 | }, 157 | render: function() { 158 | return '
' + this.data.count + '
'; 159 | }, 160 | onRender: function() { 161 | expect(this.node.innerHTML).to.be('' + this.data.count); 162 | this.data.count++; 163 | if (this.data.count > 10) { 164 | this.destroy(); 165 | done(); 166 | } 167 | } 168 | }); 169 | new Component().attachTo(this.node); 170 | }); 171 | 172 | it('should render a nested component', function(done) { 173 | var Main = hyperd.Component.extend({ 174 | render: function() { 175 | return '' + this.props.greeting + ''; 176 | } 177 | }); 178 | var App = hyperd.Component.extend({ 179 | components: { 180 | main: Main 181 | }, 182 | render: function() { 183 | return '
'; 184 | }, 185 | onRender: function() { 186 | expect(this.node.innerHTML).to.be('hi'); 187 | this.destroy(); 188 | done(); 189 | } 190 | }); 191 | new App().attachTo(this.node); 192 | }); 193 | 194 | it('should re-render a child component', function(done) { 195 | var Child = hyperd.Component.extend({ 196 | constructor: function() { 197 | hyperd.Component.apply(this, arguments); 198 | this.data.count = 0; 199 | }, 200 | render: function() { 201 | return '
' + this.data.count + '
'; 202 | }, 203 | onRender: function() { 204 | expect(this.node.innerHTML).to.be('' + this.data.count); 205 | this.data.count++; 206 | if (this.data.count > 5) { 207 | parent.destroy(); 208 | done(); 209 | } 210 | } 211 | }); 212 | var Parent = hyperd.Component.extend({ 213 | components: { 214 | child: Child 215 | }, 216 | render: function() { 217 | return '
'; 218 | } 219 | }); 220 | var parent = new Parent().attachTo(this.node); 221 | }); 222 | 223 | it('should not render when data didn\'t change', function(done) { 224 | var Component = hyperd.Component.extend({ 225 | constructor: function() { 226 | hyperd.Component.apply(this, arguments); 227 | this.called = false; 228 | }, 229 | render: function() { 230 | return '
' + this.data + '
'; 231 | }, 232 | onRender: function() { 233 | if (this.called) { 234 | expect().fail('Unexpectedly called'); 235 | return; 236 | } 237 | 238 | this.called = true; 239 | expect(this.node.innerHTML).to.be('hi') 240 | this.data = 'hi'; 241 | 242 | var self = this; 243 | // wait for possible re-rendering 244 | setTimeout(function() { 245 | self.destroy(); 246 | done(); 247 | }, 100); 248 | } 249 | }); 250 | var component = new Component().attachTo(this.node); 251 | component.data = 'hi'; 252 | }); 253 | 254 | it('should set key', function(done) { 255 | this.node.dataset.hkey = 'foo'; 256 | var Component = hyperd.Component.extend({ 257 | render: function() { 258 | return '
'; 259 | }, 260 | onRender: function() { 261 | expect(this.node.dataset.hkey).to.be('foo'); 262 | expect(this.tree.key).to.be('foo'); 263 | this.destroy(); 264 | done(); 265 | } 266 | }); 267 | new Component().attachTo(this.node); 268 | }); 269 | }); 270 | -------------------------------------------------------------------------------- /lib/component.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var virtualDOM = require('virtual-dom'); 4 | var htmlToVdom = require('html-to-vdom'); 5 | var delegate = require('dom-delegate'); 6 | var raf = require('raf'); 7 | var clone = require('clone'); 8 | var equal = require('deep-equal'); 9 | var extend = require('xtend/mutable'); 10 | var trim = require('trim'); 11 | var Widget = require('./widget'); 12 | var slice = Array.prototype.slice; 13 | var emit = EventEmitter.prototype.emit; 14 | var on = EventEmitter.prototype.on; 15 | 16 | var convertHTML = htmlToVdom({ 17 | VNode: virtualDOM.VNode, 18 | VText: virtualDOM.VText 19 | }); 20 | 21 | var events = [ 22 | 'attach', 23 | 'render', 24 | 'destroy' 25 | ]; 26 | 27 | module.exports = Component; 28 | 29 | util.inherits(Component, EventEmitter); 30 | 31 | Component.extend = function(proto) { 32 | var Parent = this; 33 | 34 | var Child; 35 | if (proto.hasOwnProperty('constructor')) { 36 | Child = proto.constructor; 37 | } else { 38 | Child = function() { 39 | return Parent.apply(this, arguments); 40 | }; 41 | } 42 | 43 | util.inherits(Child, Parent); 44 | 45 | extend(Child, Parent); 46 | extend(Child.prototype, proto); 47 | 48 | return Child; 49 | }; 50 | 51 | function Component(props) { 52 | EventEmitter.call(this); 53 | 54 | this.data = {}; 55 | this.oldData = null; 56 | this.node = null; 57 | this.tree = null; 58 | this.requestId = null; 59 | this.isDirty = false; 60 | this.components = this.components || {}; 61 | this.widgets = []; 62 | this.delegate = delegate(); 63 | this.setProps(props); 64 | } 65 | 66 | /** 67 | * @api public 68 | */ 69 | 70 | Component.prototype.attachTo = function(node) { 71 | this.node = node; 72 | this.tree = convertHTML({ getVNodeKey: getVNodeKey }, node.outerHTML); 73 | this.delegate.root(node); 74 | this.emitAttach(); 75 | this.runLoop(); 76 | return this; 77 | }; 78 | 79 | /** 80 | * @api public 81 | */ 82 | 83 | Component.prototype.destroy = function() { 84 | raf.cancel(this.requestId); 85 | 86 | // destroy child components 87 | var widgets = this.widgets; 88 | for (var i = 0, len = widgets.length; i < len; i++) { 89 | var component = widgets[i].component; 90 | component && component.destroy(); 91 | } 92 | 93 | this.props = null; 94 | this.data = null; 95 | this.oldData = null; 96 | this.node = null; 97 | this.context = null; 98 | this.tree = null; 99 | this.requestId = null; 100 | this.isDirty = false; 101 | this.widgets = null; 102 | 103 | this.onDestroy && this.onDestroy(); 104 | this.emit('destroy'); 105 | 106 | this.removeAllListeners(); 107 | 108 | this.delegate.destroy(); 109 | this.delegate = null; 110 | }; 111 | 112 | Component.prototype.setProps = function(props) { 113 | props = props || {}; 114 | this.isDirty = !equal(this.props, props, { strict: true }); 115 | this.props = props; 116 | }; 117 | 118 | /** 119 | * @api public 120 | * @override 121 | */ 122 | 123 | Component.prototype.emit = function(type) { 124 | if (~events.indexOf(type)) { 125 | emit.apply(this, arguments); 126 | return this; 127 | } 128 | 129 | var detail = slice.call(arguments, 1); 130 | var event; 131 | if (global.CustomEvent) { 132 | event = new CustomEvent(type, { bubbles: true, detail: detail }); 133 | } else { 134 | event = document.createEvent('CustomEvent'); 135 | event.initCustomEvent(type, true, false, detail); 136 | } 137 | this.node.dispatchEvent(event); 138 | return this; 139 | }; 140 | 141 | /** 142 | * @api public 143 | * @override 144 | */ 145 | 146 | Component.prototype.on = function(type, selector, listener) { 147 | if ('function' === typeof selector) { 148 | listener = selector; 149 | selector = null; 150 | } 151 | 152 | if (!selector && ~events.indexOf(type)) { 153 | on.call(this, type, listener); 154 | return this; 155 | } 156 | 157 | var self = this; 158 | 159 | function g(e) { 160 | var args = [e]; 161 | if (util.isArray(e.detail)) { 162 | args = args.concat(e.detail); 163 | } 164 | listener.apply(self, args); 165 | } 166 | 167 | listener.listener = g; 168 | this.delegate.on(type, selector, g); 169 | return this; 170 | }; 171 | 172 | /** 173 | * @api public 174 | * @override 175 | */ 176 | 177 | Component.prototype.removeListener = function(type, selector, listener) { 178 | if ('function' === typeof selector) { 179 | listener = selector; 180 | selector = null; 181 | } 182 | 183 | if (!selector && ~events.indexOf(type)) { 184 | EventEmitter.prototype.removeListener.call(this, type, listener); 185 | return this; 186 | } 187 | 188 | this.delegate.off(type, selector, listener.listener || listener); 189 | return this; 190 | }; 191 | 192 | /** 193 | * @api public 194 | * @override 195 | */ 196 | 197 | Component.prototype.removeAllListeners = function(type, selector) { 198 | if (!selector) { 199 | EventEmitter.prototype.removeAllListeners.call(this, type); 200 | } 201 | 202 | this.delegate.off(type, selector); 203 | return this; 204 | }; 205 | 206 | /** 207 | * @api private 208 | */ 209 | 210 | Component.prototype.runLoop = function() { 211 | var self = this; 212 | this.requestId = raf(function tick() { 213 | self.tick(); 214 | if (!self.node) return; 215 | self.requestId = raf(tick); 216 | }); 217 | }; 218 | 219 | /** 220 | * @api private 221 | */ 222 | 223 | Component.prototype.tick = function() { 224 | if (this.doRender()) { 225 | this.emitRender(); 226 | } 227 | }; 228 | 229 | /** 230 | * @api private 231 | */ 232 | 233 | Component.prototype.tickChildren = function() { 234 | var widgets = this.widgets; 235 | for (var i = 0, len = widgets.length; i < len; i++) { 236 | var widget = widgets[i]; 237 | widget.component && widget.component.tick(); 238 | } 239 | }; 240 | 241 | /** 242 | * @api private 243 | */ 244 | 245 | Component.prototype.doRender = function() { 246 | if (!this.isDirty) { 247 | this.isDirty = !equal(this.data, this.oldData, { strict: true }); 248 | if (!this.isDirty) { 249 | this.tickChildren(); 250 | return false; 251 | } 252 | } 253 | 254 | var html = this.render(); 255 | var tree = this.createTree(html); 256 | 257 | this.applyTree(tree); 258 | 259 | this.oldData = clone(this.data); 260 | this.tree = tree; 261 | this.isDirty = false; 262 | 263 | return true; 264 | }; 265 | 266 | /** 267 | * @api private 268 | */ 269 | 270 | Component.prototype.inflate = function(tree) { 271 | if (!tree.tagName) return tree; 272 | 273 | var components = this.components; 274 | var tagName = tree.tagName.toLowerCase(); 275 | for (var _tagName in components) { 276 | if (!components.hasOwnProperty(_tagName)) continue; 277 | if (_tagName.toLowerCase() !== tagName) continue; 278 | var widget = new Widget(components[_tagName], tree.properties); 279 | this.widgets.push(widget); 280 | return widget; 281 | } 282 | 283 | var children = tree.children || []; 284 | for (var i = 0, len = children.length; i < len; i++) { 285 | children[i] = this.inflate(children[i]); 286 | } 287 | return tree; 288 | }; 289 | 290 | /** 291 | * @api private 292 | */ 293 | 294 | Component.prototype.createTree = function(html) { 295 | var tree = convertHTML({ getVNodeKey: getVNodeKey }, trim(html)); 296 | if (this.components) { 297 | this.widgets = []; 298 | tree = this.inflate(tree); 299 | } 300 | return tree; 301 | }; 302 | 303 | /** 304 | * @api private 305 | */ 306 | 307 | Component.prototype.applyTree = function(tree) { 308 | if (this.node) { 309 | var patches = virtualDOM.diff(this.tree, tree); 310 | virtualDOM.patch(this.node, patches); 311 | } else { 312 | this.node = virtualDOM.create(tree); 313 | this.delegate.root(this.node); 314 | this.emitAttach(); 315 | } 316 | }; 317 | 318 | /** 319 | * @api private 320 | */ 321 | 322 | Component.prototype.emitAttach = function() { 323 | this.onAttach && this.onAttach(); 324 | this.emit('attach'); 325 | }; 326 | 327 | /** 328 | * @api private 329 | */ 330 | 331 | Component.prototype.emitRender = function() { 332 | var widgets = this.widgets; 333 | for (var i = 0, len = widgets.length; i < len; i++) { 334 | var widget = widgets[i]; 335 | if (widget.rendered) { 336 | widget.component.emitRender(); 337 | } 338 | } 339 | 340 | this.onRender && this.onRender(); 341 | this.emit('render'); 342 | }; 343 | 344 | function getVNodeKey(properties) { 345 | if (properties.dataset) { 346 | return properties.dataset.hkey; 347 | } 348 | } 349 | --------------------------------------------------------------------------------