├── .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 | [](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 '' + items.map(function(item) {
246 | return '- ' + item + '
';
247 | }).join('') + '
';
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 |
--------------------------------------------------------------------------------