├── .babelrc
├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── README.md
├── benchmark
├── store.es6.js
├── vnode.js
└── vpatch.js
├── dist
├── index.js
└── misstime.js
├── gulpfile.js
├── karma.benchmark.conf.js
├── karma.conf.js
├── karma.mocha.conf.js
├── karma.server.conf.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── event.js
├── hydration.js
├── index.js
├── tostring.js
├── utils.js
├── vdom.js
├── vnode.js
├── vpatch.js
└── wrappers
│ ├── input.js
│ ├── process.js
│ ├── select.js
│ └── textarea.js
├── test
├── hydration.js
├── patch.js
├── render.js
├── tostring.js
├── utils.js
└── vnode.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015-loose"],
3 | "plugins": [
4 | "transform-es3-property-literals",
5 | "transform-es3-member-expression-literals"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | dist/test.js
4 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 6,
3 | "esnext": true
4 | }
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | benchmark
2 | script
3 | test
4 | .babelrc
5 | node_modules
6 | .jshintrc
7 | npm-debug.log
8 | karma*
9 | gulpfile.js
10 | rollup.config.js
11 | webpack.config.js
12 | .travis.yml
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 10
4 | addons:
5 | - firefox: "latest"
6 | - sauce_connect: true
7 | before_script:
8 | - export CHROME_BIN=chromium-browser
9 | - export DISPLAY=:99.0
10 | - sh -e /etc/init.d/xvfb start
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | # MissTime
12 |
13 | A virtual-dom library forked from [inferno][1] and inspired by [virtual-dom][2].
14 |
15 |
16 | [1]: https://github.com/infernojs/inferno
17 | [2]: https://github.com/Matt-Esch/virtual-dom
18 |
19 |
--------------------------------------------------------------------------------
/benchmark/store.es6.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function _random(max) {
4 | return Math.round(Math.random()*1000)%max;
5 | }
6 |
7 | export class Store {
8 | constructor() {
9 | this.data = [];
10 | this.selected = undefined;
11 | this.id = 1;
12 | }
13 | buildData(count = 1000) {
14 | var adjectives = ["pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
15 | var colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
16 | var nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
17 | var data = [];
18 | for (var i = 0; i < count; i++)
19 | data.push({id: this.id++, label: adjectives[_random(adjectives.length)] + " " + colours[_random(colours.length)] + " " + nouns[_random(nouns.length)] });
20 | return data;
21 | }
22 | updateData(mod = 10) {
23 | for (let i=0;i d.id==id);
29 | this.data = this.data.filter((e,i) => i!=idx);
30 | return this;
31 | }
32 | run() {
33 | this.data = this.buildData();
34 | this.selected = undefined;
35 | }
36 | add(count = 1000) {
37 | this.data = this.data.concat(this.buildData(count));
38 | }
39 | update() {
40 | this.updateData();
41 | }
42 | select(id) {
43 | this.selected = id;
44 | }
45 | hideAll() {
46 | this.backup = this.data;
47 | this.data = [];
48 | this.selected = undefined;
49 | }
50 | showAll() {
51 | this.data = this.backup;
52 | this.backup = null;
53 | this.selected = undefined;
54 | }
55 | runLots() {
56 | this.data = this.buildData(10000);
57 | this.selected = undefined;
58 | }
59 | clear() {
60 | this.data = [];
61 | this.selected = undefined;
62 | }
63 | swapRows() {
64 | if(this.data.length > 10) {
65 | var a = this.data[4];
66 | this.data[4] = this.data[9];
67 | this.data[9] = a;
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/benchmark/vnode.js:
--------------------------------------------------------------------------------
1 | import {Store} from './store.es6';
2 | // import {createVNode, render as mount} from 'inferno';
3 | // import {mount} from 'inferno/dist/DOM/mounting';
4 | import {h} from '../src/index';
5 | import {createElement as r} from '../src/vdom';
6 |
7 | const store = new Store();
8 | store.add();
9 | process.env.NODE_ENV = 'production';
10 | suite('test', () => {
11 | function createRows() {
12 | var rows = [];
13 | var data = store.data;
14 | var selected = store.selected;
15 |
16 | for (var i = 0; i < data.length; i++) {
17 | var d = data[i];
18 | var id = d.id;
19 |
20 | rows.push(
21 | createVNode(66, 'tr', id === selected ? 'danger' : '', [
22 | createVNode(2, 'td', 'col-md-1', id + ''),
23 | createVNode(2, 'td', 'col-md-4',
24 | createVNode(2, 'a', null, d.label)),
25 | createVNode(2, 'td', 'col-md-1',
26 | createVNode(2, 'a', null,
27 | createVNode(2, 'span', 'glyphicon glyphicon-remove', null, {
28 | 'aria-hidden': 'true'
29 | }))),
30 | createVNode(66, 'td', 'col-md-6')
31 | ])
32 | );
33 | }
34 | return createVNode(2, 'tbody', null, rows);
35 | }
36 |
37 | function createRowsByMiss() {
38 | var rows = [];
39 | var data = store.data;
40 | var selected = store.selected;
41 |
42 | for (var i = 0; i < data.length; i++) {
43 | var d = data[i];
44 | var id = d.id;
45 |
46 | rows.push(
47 | h('tr', null, [
48 | h('td', null, id + '', 'col-md-1'),
49 | h('td', null,
50 | h('a', null, d.label)
51 | , 'col-md-4'),
52 | h('td', null,
53 | h('a', null,
54 | h('span',
55 | {
56 | 'aria-hidden': 'true'
57 | }, null, 'glyphicon glyphicon-remove'
58 | )
59 | )
60 | , 'col-md-1'),
61 | h('td', null, null, 'col-md-6')
62 | ], id === selected ? 'danger' : '')
63 | );
64 | }
65 | return h('tbody', null, rows);
66 | }
67 |
68 | // benchmark('inferno', () => {
69 | // createRows();
70 | // });
71 |
72 | // benchmark('miss', () => {
73 | // var a = createRowsByMiss();
74 | // });
75 |
76 | // benchmark('inferno render', () => {
77 | // const vNodes = createRows();
78 | // mount(vNodes, document.createElement('div'));
79 | // });
80 |
81 | benchmark('miss render', () => {
82 | const vNodes1 = createRowsByMiss();
83 | r(vNodes1, document.createElement('div'));
84 | });
85 |
86 | benchmark('push', () => {
87 | var a = [];
88 | for (var i = 0; i < 10000; i++) {
89 | a.push(i)
90 | }
91 | });
92 |
93 | benchmark('concat', () => {
94 | var a = [];
95 | for (var i = 0; i < 1; i++) {
96 | a = a.concat(new Array(10000));
97 | }
98 | })
99 | });
100 |
--------------------------------------------------------------------------------
/benchmark/vpatch.js:
--------------------------------------------------------------------------------
1 | import {Store} from './store.es6';
2 | import {createVNode} from 'inferno';
3 | import {mount} from 'inferno/dist/DOM/mounting';
4 | import {render} from 'inferno/dist/DOM/rendering';
5 | import {h} from '../src/index';
6 | import {render as r} from '../src/vdom';
7 | import {patch as p} from '../src/vpatch';
8 |
9 | const store = new Store();
10 | // store.add(2);
11 | store.runLots();
12 | process.env.NODE_ENV = 'production';
13 |
14 | function createRows() {
15 | var rows = [];
16 | var data = store.data;
17 | var selected = store.selected;
18 |
19 | for (var i = 0; i < data.length; i++) {
20 | var d = data[i];
21 | var id = d.id;
22 |
23 | rows.push(
24 | createVNode(66, 'tr', id === selected ? 'danger' : '', [
25 | createVNode(2, 'td', 'col-md-1', id + ''),
26 | createVNode(2, 'td', 'col-md-4',
27 | createVNode(2, 'a', null, d.label)),
28 | createVNode(2, 'td', 'col-md-1',
29 | createVNode(2, 'a', null,
30 | createVNode(2, 'span', 'glyphicon glyphicon-remove', null, {
31 | 'aria-hidden': 'true'
32 | }))),
33 | createVNode(66, 'td', 'col-md-6')
34 | ])
35 | );
36 | }
37 | return createVNode(2, 'tbody', null, rows);
38 | }
39 |
40 | // function createRowsByMiss() {
41 | // var rows = [];
42 | // var data = store.data;
43 | // var selected = store.selected;
44 |
45 | // for (var i = 0; i < data.length; i++) {
46 | // var d = data[i];
47 | // var id = d.id;
48 |
49 | // rows.push(
50 | // h('tr', {className: id === selected ? 'danger' : ''}, [
51 | // h('td', {className: 'col-md-1'}, id + ''),
52 | // h('td', {className: 'col-md-4'},
53 | // h('a', null, d.label)
54 | // ),
55 | // h('td', {className: 'col-md-1'},
56 | // h('a', null,
57 | // h('span', {
58 | // className: 'glyphicon glyphicon-remove',
59 | // 'aria-hidden': 'true'
60 | // })
61 | // )
62 | // ),
63 | // h('td', {className: 'col-md-6'})
64 | // ])
65 | // );
66 | // }
67 | // return h('tbody', null, rows);
68 | // }
69 |
70 | function createRowsByMiss() {
71 | var rows = [];
72 | var data = store.data;
73 | var selected = store.selected;
74 |
75 | for (var i = 0; i < data.length; i++) {
76 | var d = data[i];
77 | var id = d.id;
78 |
79 | rows.push(
80 | h('tr', null, [
81 | h('td', null, id + '', 'col-md-1'),
82 | h('td', null,
83 | h('a', null, d.label)
84 | , 'col-md-4'),
85 | h('td', null,
86 | h('a', null,
87 | h('span',
88 | {
89 | 'aria-hidden': 'true'
90 | }, null, 'glyphicon glyphicon-remove'
91 | )
92 | )
93 | , 'col-md-1'),
94 | h('td', null, null, 'col-md-6')
95 | ], id === selected ? 'danger' : '')
96 | );
97 | }
98 | return h('tbody', null, rows);
99 | }
100 |
101 | window.run = function() {
102 | console.time('a')
103 | const vNodes1 = createRows();
104 | const dom = document.createElement('div');
105 | render(vNodes1, dom);
106 | store.select(1);
107 | const vNodes2 = createRows();
108 | render(vNodes2, dom);
109 | console.timeEnd('a')
110 | }
111 |
112 | window.runMiss = function() {
113 | console.time('b')
114 | const vNodes1 = createRowsByMiss();
115 | const dom = document.createElement('div');
116 | r(vNodes1, dom);
117 | store.select(2);
118 | const vNodes2 = createRowsByMiss();
119 | p(vNodes1, vNodes2);
120 | console.timeEnd('b')
121 | }
122 |
123 |
124 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var babel = require('gulp-babel');
3 |
4 | gulp.task('default', function() {
5 | return gulp.src('src/**/*.js')
6 | .pipe(babel())
7 | .pipe(gulp.dest('dist'));
8 | });
9 |
--------------------------------------------------------------------------------
/karma.benchmark.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | module.exports = function(config) {
4 | config.set({
5 | logLevel: config.LOG_INFO,
6 | files: [
7 | 'node_modules/sinon/pkg/sinon.js',
8 | 'benchmark/vnode.js'
9 | ],
10 | preprocessors: {
11 | 'benchmark/**/*.js': ['webpack']
12 | },
13 | webpack: {
14 | module: {
15 | rules: [
16 | {
17 | test: /\.js$/,
18 | loader: 'babel-loader',
19 | exclude: /node-modules/
20 | }
21 | ]
22 | }
23 | },
24 | frameworks: [
25 | 'benchmark',
26 | ],
27 | reporters: [
28 | 'benchmark',
29 | ],
30 | plugins: [
31 | 'karma-chrome-launcher',
32 | 'karma-webpack',
33 | 'karma-benchmark',
34 | 'karma-benchmark-reporter',
35 | ],
36 | browser: ['chrome'],
37 | client: {
38 | mocha: {
39 | reporter: 'html'
40 | }
41 | }
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | process.env.NODE_ENV = 'development';
4 |
5 | module.exports = function(config) {
6 | config.set({
7 | logLevel: config.LOG_INFO,
8 | files: [
9 | // 'node_modules/babel-polyfill/dist/polyfill.js',
10 | 'node_modules/sinon/pkg/sinon.js',
11 | 'test/**/*.js',
12 | // 'test/render.js',
13 | ],
14 | preprocessors: {
15 | 'test/**/*.js': ['webpack'],
16 | },
17 | webpack: {
18 | module: {
19 | rules: [
20 | {
21 | test: /\.js$/,
22 | loader: 'babel-loader',
23 | exclude: /node_modules/
24 | }
25 | ]
26 | },
27 | devtool: '#inline-source-map',
28 | plugins: [
29 | new webpack.DefinePlugin({
30 | 'process.env': {
31 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
32 | }
33 | }),
34 | ]
35 | },
36 | frameworks: [
37 | 'mocha',
38 | ],
39 | plugins: [
40 | 'karma-chrome-launcher',
41 | 'karma-firefox-launcher',
42 | 'karma-sauce-launcher',
43 | 'karma-mocha',
44 | 'karma-webpack',
45 | ],
46 | sauceLabs: {
47 | testName: 'Miss Unit Tests'
48 | },
49 | client: {
50 | mocha: {
51 | reporter: 'html'
52 | }
53 | },
54 | concurrency: 2,
55 | singleRun: true
56 | });
57 | };
58 |
--------------------------------------------------------------------------------
/karma.mocha.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const commonConfig = require('./karma.conf');
3 |
4 | const customLaunchers = {
5 | sl_chrome: {
6 | base: 'SauceLabs',
7 | browserName: 'chrome',
8 | platform: 'Windows 7',
9 | // version: '58'
10 | },
11 | sl_firefox: {
12 | base: 'SauceLabs',
13 | browserName: 'firefox',
14 | platform: 'Windows 7',
15 | // version: '53'
16 | },
17 | // sl_opera: {
18 | // base: 'SauceLabs',
19 | // browserName: 'opera',
20 | // platform: 'Windows 7',
21 | // // version: '12'
22 | // },
23 | sl_safari: {
24 | base: 'SauceLabs',
25 | browserName: 'safari',
26 | platform: 'macOS 10.12',
27 | version: '10'
28 | },
29 | // sl_ios_safari_9: {
30 | // base: 'SauceLabs',
31 | // browserName: 'iphone',
32 | // version: '10.3'
33 | // },
34 | // 'SL_ANDROID4.4': {
35 | // base: 'SauceLabs',
36 | // browserName: 'android',
37 | // platform: 'Linux',
38 | // version: '4.4'
39 | // },
40 | SL_ANDROID5: {
41 | base: 'SauceLabs',
42 | browserName: 'android',
43 | platform: 'Linux',
44 | version: '5.1'
45 | },
46 | SL_ANDROID6: {
47 | base: 'SauceLabs',
48 | browserName: 'Chrome',
49 | platform: 'Android',
50 | version: '6.0',
51 | device: 'Android Emulator'
52 | },
53 | };
54 |
55 | [9, 10, 11, 13, 14].forEach(v => {
56 | customLaunchers[`sl_ie${v}`] = {
57 | base: 'SauceLabs',
58 | browserName: v === 13 || v === 14 ? 'MicrosoftEdge' : 'internet explorer',
59 | platform: `Windows ${v === 13 || v === 14 ? '10' : '7'}`,
60 | version: v
61 | };
62 | });
63 |
64 | module.exports = function(config) {
65 | commonConfig(config);
66 | config.set({
67 | browsers: Object.keys(customLaunchers),
68 | customLaunchers: customLaunchers,
69 | reporters: ['dots', 'saucelabs'],
70 | });
71 | };
72 |
--------------------------------------------------------------------------------
/karma.server.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const commonConfig = require('./karma.conf');
3 |
4 | module.exports = function(config) {
5 | commonConfig(config);
6 | };
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "misstime",
3 | "version": "0.3.18",
4 | "description": "",
5 | "main": "dist/index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/Javey/misstime"
12 | },
13 | "scripts": {
14 | "test": "karma start karma.mocha.conf.js",
15 | "test:local": "karma start karma.conf.js",
16 | "build": "rollup -c rollup.config.js",
17 | "karma:benchmark": "karma start karma.benchmark.conf.js",
18 | "karma:test": "karma start karma.server.conf.js",
19 | "release": "npm run release-patch",
20 | "prelease": "npm version prerelease && git push --tags --force && git push && npm publish",
21 | "release-patch": "git checkout master && git push && npm version patch && git push --tags && git push && npm publish",
22 | "release-minor": "git checkout master && git push && npm version minor && git push --tags && git push && npm publish",
23 | "release-major": "git checkout master && git push && npm version major && git push --tags && git push && npm publish"
24 | },
25 | "author": "javey",
26 | "license": "MIT",
27 | "devDependencies": {
28 | "babel-core": "^6.26.3",
29 | "babel-loader": "^7.0.0",
30 | "babel-plugin-external-helpers": "^6.22.0",
31 | "babel-plugin-minify-constant-folding": "0.0.4",
32 | "babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
33 | "babel-plugin-transform-es3-property-literals": "^6.22.0",
34 | "babel-preset-es2015": "^6.24.1",
35 | "babel-preset-es2015-loose": "^8.0.0",
36 | "benchmark": "^2.1.4",
37 | "fsevents": "^2.3.2",
38 | "gulp": "^4.0.0",
39 | "gulp-babel": "^6.1.2",
40 | "inferno": "^5.6.2",
41 | "karma": "^3.0.0",
42 | "karma-benchmark": "^1.0.0",
43 | "karma-benchmark-reporter": "^0.1.1",
44 | "karma-chrome-launcher": "^2.0.0",
45 | "karma-firefox-launcher": "^1.0.1",
46 | "karma-html-reporter": "^0.2.7",
47 | "karma-mocha": "^1.3.0",
48 | "karma-sauce-launcher": "^1.2.0",
49 | "karma-webpack": "^3.0.0",
50 | "mocha": "^4.1.0",
51 | "rollup": "^0.42.0",
52 | "rollup-plugin-babel": "^2.7.1",
53 | "rollup-plugin-replace": "^1.1.1",
54 | "sinon": "^1.17.7",
55 | "webpack": "^2.4.1"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | const babel = require('rollup-plugin-babel');
2 | const replace = require('rollup-plugin-replace');
3 |
4 | const config = {
5 | entry: 'src/index.js',
6 | plugins: [
7 | babel({
8 | exclude: 'node_modules/**',
9 | presets: [
10 | ['es2015', {"modules": false, "loose": true}]
11 | ],
12 | plugins: [
13 | "external-helpers",
14 | "minify-constant-folding",
15 | "transform-es3-property-literals",
16 | "transform-es3-member-expression-literals",
17 | ],
18 | babelrc: false
19 | })
20 | ]
21 | };
22 |
23 |
24 | module.exports = [
25 | Object.assign({}, config, {
26 | dest: 'dist/misstime.js',
27 | format: 'umd',
28 | moduleName: 'misstime',
29 | legacy: true,
30 | }),
31 | Object.assign({}, config, {
32 | dest: 'dist/index.js',
33 | format: 'cjs'
34 | }),
35 | ];
36 |
--------------------------------------------------------------------------------
/src/event.js:
--------------------------------------------------------------------------------
1 | import {
2 | SimpleMap, isNullOrUndefined, createObject,
3 | doc as document, browser, isArray, config
4 | } from './utils';
5 |
6 | function preventDefault() {
7 | this.returnValue = false;
8 | }
9 |
10 | function stopPropagation() {
11 | this.cancelBubble = true;
12 | this.stopImmediatePropagation && this.stopImmediatePropagation();
13 | }
14 |
15 | let addEventListener;
16 | let removeEventListener;
17 | function fixEvent(fn) {
18 | if (!fn._$cb) {
19 | fn._$cb = (event) => {
20 | // for compatibility
21 | event._rawEvent = event
22 |
23 | event.stopPropagation = stopPropagation;
24 | if (!event.preventDefault) {
25 | event.preventDefault = preventDefault;
26 | }
27 | fn(event);
28 | }
29 | }
30 | return fn._$cb;
31 | }
32 | if ('addEventListener' in document) {
33 | addEventListener = function(dom, name, fn) {
34 | fn = fixEvent(fn);
35 | dom.addEventListener(name, fn, false);
36 | };
37 |
38 | removeEventListener = function(dom, name, fn) {
39 | dom.removeEventListener(name, fn._$cb || fn);
40 | };
41 | } else {
42 | addEventListener = function(dom, name, fn) {
43 | fn = fixEvent(fn);
44 | dom.attachEvent(`on${name}`, fn);
45 | };
46 |
47 | removeEventListener = function(dom, name, fn) {
48 | dom.detachEvent(`on${name}`, fn._$cb || fn);
49 | };
50 | }
51 |
52 | const delegatedEvents = {};
53 | const unDelegatesEvents = {
54 | 'mouseenter': true,
55 | 'mouseleave': true,
56 | 'propertychange': true,
57 | 'scroll': true,
58 | 'wheel': true,
59 | };
60 |
61 | // change event can not be deletegated in IE8
62 | if (browser.isIE8) {
63 | unDelegatesEvents.change = true;
64 | }
65 |
66 | export function handleEvent(name, lastEvent, nextEvent, dom) {
67 | // debugger;
68 | if (name === 'blur') {
69 | name = 'focusout';
70 | } else if (name === 'focus') {
71 | name = 'focusin';
72 | } else if (browser.isIE8 && name === 'input') {
73 | name = 'propertychange';
74 | }
75 |
76 | if (!config.disableDelegate && !unDelegatesEvents[name]) {
77 | let delegatedRoots = delegatedEvents[name];
78 |
79 | if (nextEvent) {
80 | if (!delegatedRoots) {
81 | delegatedRoots = {items: new SimpleMap(), docEvent: null};
82 | delegatedRoots.docEvent = attachEventToDocument(name, delegatedRoots);
83 | delegatedEvents[name] = delegatedRoots;
84 | }
85 | delegatedRoots.items.set(dom, nextEvent);
86 | } else if (delegatedRoots) {
87 | const items = delegatedRoots.items;
88 | if (items.delete(dom)) {
89 | if (items.size === 0) {
90 | removeEventListener(document, name, delegatedRoots.docEvent);
91 | delete delegatedEvents[name];
92 | }
93 | }
94 | }
95 | } else {
96 | if (lastEvent) {
97 | if (isArray(lastEvent)) {
98 | for (let i = 0; i < lastEvent.length; i++) {
99 | if (lastEvent[i]) {
100 | removeEventListener(dom, name, lastEvent[i]);
101 | }
102 | }
103 | } else {
104 | removeEventListener(dom, name, lastEvent);
105 | }
106 | }
107 | if (nextEvent) {
108 | if (isArray(nextEvent)) {
109 | for (let i = 0; i < nextEvent.length; i++) {
110 | if (nextEvent[i]) {
111 | addEventListener(dom, name, nextEvent[i]);
112 | }
113 | }
114 | } else {
115 | addEventListener(dom, name, nextEvent);
116 | }
117 | }
118 | }
119 | }
120 |
121 | function dispatchEvent(event, target, items, count, isClick, eventData) {
122 | // if event has cancelled bubble, return directly
123 | // otherwise it is also triggered sometimes, e.g in React
124 | if (event.cancelBubble) {
125 | return;
126 | }
127 |
128 | const eventToTrigger = items.get(target);
129 | if (eventToTrigger) {
130 | count--;
131 | eventData.dom = target;
132 | // for fallback when Object.defineProperty is undefined
133 | event._currentTarget = target;
134 | if (isArray(eventToTrigger)) {
135 | for (let i = 0; i < eventToTrigger.length; i++) {
136 | const _eventToTrigger = eventToTrigger[i];
137 | if (_eventToTrigger) {
138 | _eventToTrigger(event);
139 | }
140 | }
141 | } else {
142 | eventToTrigger(event);
143 | }
144 | }
145 | if (count > 0) {
146 | const parentDom = target.parentNode;
147 | if (isNullOrUndefined(parentDom) || (isClick && parentDom.nodeType === 1 && parentDom.disabled)) {
148 | return;
149 | }
150 | dispatchEvent(event, parentDom, items, count, isClick, eventData);
151 | }
152 | }
153 |
154 | function attachEventToDocument(name, delegatedRoots) {
155 | var docEvent = function(event) {
156 | const count = delegatedRoots.items.size;
157 | if (count > 0) {
158 | const eventData = {
159 | dom: document
160 | };
161 | try {
162 | Object.defineProperty(event, 'currentTarget', {
163 | configurable: true,
164 | get() {
165 | return eventData.dom;
166 | }
167 | });
168 | } catch (e) {
169 | // ie8
170 | }
171 | // nt._rawEvent = event
172 | dispatchEvent(
173 | event,
174 | event.target,
175 | delegatedRoots.items,
176 | count,
177 | event.type === 'click',
178 | eventData
179 | );
180 | }
181 | };
182 | addEventListener(config.delegateTarget, name, docEvent);
183 | return docEvent;
184 | }
185 |
--------------------------------------------------------------------------------
/src/hydration.js:
--------------------------------------------------------------------------------
1 | import {Types, EMPTY_OBJ} from './vnode';
2 | import {
3 | createElement, createRef,
4 | createTextElement, createCommentElement,
5 | render, createOrHydrateComponentClassOrInstance
6 | } from './vdom';
7 | import {
8 | isNullOrUndefined, setTextContent,
9 | isStringOrNumber, isArray, MountedQueue
10 | } from './utils';
11 | import {patchProp} from './vpatch';
12 | import {processForm} from './wrappers/process';
13 |
14 | export function hydrateRoot(vNode, parentDom, mountedQueue) {
15 | if (!isNullOrUndefined(parentDom)) {
16 | let dom = parentDom.firstChild;
17 | if (isNullOrUndefined(dom)) {
18 | return render(vNode, parentDom, mountedQueue, null, false);
19 | }
20 | let newDom = hydrate(vNode, dom, mountedQueue, parentDom, null, false);
21 | dom = dom.nextSibling;
22 | // should only one entry
23 | while (dom) {
24 | let next = dom.nextSibling;
25 | parentDom.removeChild(dom);
26 | dom = next;
27 | }
28 | return newDom;
29 | }
30 | return null;
31 | }
32 |
33 | export function hydrate(vNode, dom, mountedQueue, parentDom, parentVNode, isSVG) {
34 | if (dom !== null) {
35 | let isTrigger = true;
36 | if (mountedQueue) {
37 | isTrigger = false;
38 | } else {
39 | mountedQueue = new MountedQueue();
40 | }
41 | dom = hydrateElement(vNode, dom, mountedQueue, parentDom, parentVNode, isSVG);
42 | if (isTrigger) {
43 | mountedQueue.trigger();
44 | }
45 | }
46 | return dom;
47 | }
48 |
49 | export function hydrateElement(vNode, dom, mountedQueue, parentDom, parentVNode, isSVG) {
50 | const type = vNode.type;
51 |
52 | if (type & Types.Element) {
53 | return hydrateHtmlElement(vNode, dom, mountedQueue, parentDom, parentVNode, isSVG);
54 | } else if (type & Types.Text) {
55 | return hydrateText(vNode, dom);
56 | } else if (type & Types.HtmlComment) {
57 | return hydrateComment(vNode, dom);
58 | } else if (type & Types.ComponentClassOrInstance) {
59 | return hydrateComponentClassOrInstance(vNode, dom, mountedQueue, parentDom, parentVNode, isSVG);
60 | }
61 | }
62 |
63 | function hydrateComponentClassOrInstance(vNode, dom, mountedQueue, parentDom, parentVNode, isSVG) {
64 | return createOrHydrateComponentClassOrInstance(vNode, parentDom, mountedQueue, null, true, parentVNode, isSVG, (instance) => {
65 | const newDom = instance.hydrate(vNode, dom);
66 | if (dom !== newDom && dom.parentNode) {
67 | dom.parentNode.replaceChild(newDom, dom);
68 | }
69 |
70 | return newDom;
71 | });
72 | }
73 |
74 | function hydrateComment(vNode, dom) {
75 | if (dom.nodeType !== 8) {
76 | const newDom = createCommentElement(vNode, null);
77 | dom.parentNode.replaceChild(newDom, dom);
78 | return newDom;
79 | }
80 | const comment = vNode.children;
81 | if (dom.data !== comment) {
82 | dom.data = comment;
83 | }
84 | vNode.dom = dom;
85 | return dom;
86 | }
87 |
88 | function hydrateText(vNode, dom) {
89 | if (dom.nodeType !== 3) {
90 | const newDom = createTextElement(vNode, null);
91 | dom.parentNode.replaceChild(newDom, dom);
92 |
93 | return newDom;
94 | }
95 |
96 | const text = vNode.children;
97 | if (dom.nodeValue !== text) {
98 | dom.nodeValue = text;
99 | }
100 | vNode.dom = dom;
101 |
102 | return dom;
103 | }
104 |
105 | function hydrateHtmlElement(vNode, dom, mountedQueue, parentDom, parentVNode, isSVG) {
106 | const children = vNode.children;
107 | const props = vNode.props;
108 | const className = vNode.className;
109 | const type = vNode.type;
110 | const ref = vNode.ref;
111 |
112 | vNode.parentVNode = parentVNode;
113 | isSVG = isSVG || (type & Types.SvgElement) > 0;
114 |
115 | if (dom.nodeType !== 1 || dom.tagName.toLowerCase() !== vNode.tag) {
116 | warning('Server-side markup doesn\'t match client-side markup');
117 | const newDom = createElement(vNode, null, mountedQueue, parentDom, parentVNode, isSVG);
118 | dom.parentNode.replaceChild(newDom, dom);
119 |
120 | return newDom;
121 | }
122 |
123 | vNode.dom = dom;
124 | if (!isNullOrUndefined(children)) {
125 | hydrateChildren(children, dom, mountedQueue, vNode, isSVG);
126 | } else if (dom.firstChild !== null) {
127 | setTextContent(dom, '');
128 | }
129 |
130 | if (props !== EMPTY_OBJ) {
131 | const isFormElement = (type & Types.FormElement) > 0;
132 | for (let prop in props) {
133 | patchProp(prop, null, props[prop], dom, isFormElement, isSVG);
134 | }
135 | if (isFormElement) {
136 | processForm(vNode, dom, props, true);
137 | }
138 | }
139 |
140 | if (!isNullOrUndefined(className)) {
141 | if (isSVG) {
142 | dom.setAttribute('class', className);
143 | } else {
144 | dom.className = className;
145 | }
146 | } else if (dom.className !== '') {
147 | dom.removeAttribute('class');
148 | }
149 |
150 | if (ref) {
151 | createRef(dom, ref, mountedQueue);
152 | }
153 |
154 | return dom;
155 | }
156 |
157 | function hydrateChildren(children, parentDom, mountedQueue, parentVNode, isSVG) {
158 | normalizeChildren(parentDom);
159 | let dom = parentDom.firstChild;
160 |
161 | if (isStringOrNumber(children)) {
162 | if (dom !== null && dom.nodeType === 3) {
163 | if (dom.nodeValue !== children) {
164 | dom.nodeValue = children;
165 | }
166 | } else if (children === '') {
167 | parentDom.appendChild(document.createTextNode(''));
168 | } else {
169 | setTextContent(parentDom, children);
170 | }
171 | if (dom !== null) {
172 | dom = dom.nextSibling;
173 | }
174 | } else if (isArray(children)) {
175 | for (let i = 0; i < children.length; i++) {
176 | const child = children[i];
177 |
178 | if (!isNullOrUndefined(child)) {
179 | if (dom !== null) {
180 | const nextSibling = dom.nextSibling;
181 | hydrateElement(child, dom, mountedQueue, parentDom, parentVNode, isSVG);
182 | dom = nextSibling;
183 | } else {
184 | createElement(child, parentDom, mountedQueue, true, parentVNode, isSVG);
185 | }
186 | }
187 | }
188 | } else {
189 | if (dom !== null) {
190 | hydrateElement(children, dom, mountedQueue, parentDom, parentVNode, isSVG);
191 | dom = dom.nextSibling;
192 | } else {
193 | createElement(children, parentDom, mountedQueue, true, parentVNode, isSVG);
194 | }
195 | }
196 |
197 | // clear any other DOM nodes, there should be on a single entry for the root
198 | while (dom) {
199 | const nextSibling = dom.nextSibling;
200 | parentDom.removeChild(dom);
201 | dom = nextSibling;
202 | }
203 | }
204 |
205 | function normalizeChildren(parentDom) {
206 | let dom = parentDom.firstChild;
207 |
208 | while (dom) {
209 | if (dom.nodeType === 8 && dom.data === '') {
210 | const lastDom = dom.previousSibling;
211 | parentDom.removeChild(dom);
212 | dom = lastDom || parentDom.firstChild;
213 | } else {
214 | dom = dom.nextSibling;
215 | }
216 | }
217 | }
218 |
219 | const warning = typeof console === 'object' ? function(message) {
220 | console.warn(message);
221 | } : function() {};
222 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | createVNode,
3 | createCommentVNode,
4 | createUnescapeTextVNode,
5 | Types,
6 | VNode,
7 | directClone
8 | } from './vnode';
9 | import {patch} from './vpatch';
10 | import {render, removeElement} from './vdom';
11 | import {MountedQueue, hooks, config} from './utils';
12 | import {toString} from './tostring';
13 | import {hydrateRoot, hydrate} from './hydration';
14 |
15 | export {
16 | createVNode as h,
17 | patch,
18 | render,
19 | createCommentVNode as hc,
20 | createUnescapeTextVNode as hu,
21 | removeElement as remove,
22 | MountedQueue,
23 | toString as renderString,
24 | hydrateRoot,
25 | hydrate,
26 | Types,
27 | VNode, // for type check
28 | hooks,
29 | directClone as clone,
30 | config,
31 | };
32 |
--------------------------------------------------------------------------------
/src/tostring.js:
--------------------------------------------------------------------------------
1 | import {Types, EMPTY_OBJ} from './vnode';
2 | import {isNullOrUndefined, isArray, selfClosingTags,
3 | isStringOrNumber
4 | } from './utils';
5 | import {kebabCase} from './vpatch';
6 |
7 | export function toString(vNode, parent, disableSplitText, firstChild) {
8 | const type = vNode.type;
9 | const tag = vNode.tag;
10 | const props = vNode.props;
11 | const children = vNode.children;
12 | vNode.parentVNode = parent;
13 |
14 | let html;
15 | if (type & Types.ComponentClass) {
16 | const instance = new tag(props);
17 | instance.parentVNode = parent;
18 | instance.vNode = vNode;
19 | vNode.children = instance;
20 | html = instance.toString();
21 | } else if (type & Types.ComponentInstance) {
22 | children.parentVNode = parent;
23 | children.vNode = vNode;
24 | html = children.toString();
25 | } else if (type & Types.Element) {
26 | let innerHTML;
27 | html = `<${tag}`;
28 |
29 | if (!isNullOrUndefined(vNode.className)) {
30 | html += ` class="${escapeText(vNode.className)}"`;
31 | }
32 |
33 | if (props !== EMPTY_OBJ) {
34 | for (let prop in props) {
35 | const value = props[prop];
36 |
37 | if (prop === 'innerHTML') {
38 | innerHTML = value;
39 | } else if (prop === 'style') {
40 | html += ` style="${renderStylesToString(value)}"`;
41 | } else if (
42 | prop === 'children' || prop === 'className' ||
43 | prop === 'key' || prop === 'ref'
44 | ) {
45 | // ignore
46 | } else if (prop === 'defaultValue') {
47 | if (isNullOrUndefined(props.value) && !isNullOrUndefined(value)) {
48 | html += ` value="${isString(value) ? escapeText(value) : value}"`;
49 | }
50 | } else if (prop === 'defaultChecked') {
51 | if (isNullOrUndefined(props.checked) && value === true) {
52 | html += ' checked';
53 | }
54 | } else if (prop === 'attributes') {
55 | html += renderAttributesToString(value);
56 | } else if (prop === 'dataset') {
57 | html += renderDatasetToString(value);
58 | } else if (tag === 'option' && prop === 'value') {
59 | html += renderAttributeToString(prop, value);
60 | if (parent && value === parent.props.value) {
61 | html += ` selected`;
62 | }
63 | } else {
64 | html += renderAttributeToString(prop, value);
65 | }
66 | }
67 | }
68 |
69 | if (selfClosingTags[tag]) {
70 | html += ` />`;
71 | } else {
72 | html += '>';
73 | if (innerHTML) {
74 | html += innerHTML;
75 | } else if (!isNullOrUndefined(children)) {
76 | if (isString(children)) {
77 | html += children === '' ? ' ' : escapeText(children);
78 | } else if (isNumber(children)) {
79 | html += children;
80 | } else if (isArray(children)) {
81 | let index = -1;
82 | for (let i = 0; i < children.length; i++) {
83 | const child = children[i];
84 | if (isString(child)) {
85 | html += child === '' ? ' ' : escapeText(child);
86 | } else if (isNumber(child)) {
87 | html += child;
88 | } else if (!isNullOrUndefined(child)) {
89 | if (!(child.type & Types.Text)) {
90 | index = -1;
91 | } else {
92 | index++;
93 | }
94 | html += toString(child, vNode, disableSplitText, index === 0);
95 | }
96 | }
97 | } else {
98 | html += toString(children, vNode, disableSplitText, true);
99 | }
100 | }
101 |
102 | html += `${tag}>`;
103 | }
104 | } else if (type & Types.Text) {
105 | html = (firstChild || disableSplitText ? '' : '') +
106 | (children === '' ? ' ' : escapeText(children));
107 | } else if (type & Types.HtmlComment) {
108 | html = ``;
109 | } else if (type & Types.UnescapeText) {
110 | html = isNullOrUndefined(children) ? '' : children;
111 | } else {
112 | throw new Error(`Unknown vNode: ${vNode}`);
113 | }
114 |
115 | return html;
116 | }
117 |
118 | export function escapeText(text) {
119 | let result = text;
120 | let escapeString = "";
121 | let start = 0;
122 | let i;
123 | for (i = 0; i < text.length; i++) {
124 | switch (text.charCodeAt(i)) {
125 | case 34: // "
126 | escapeString = """;
127 | break;
128 | case 39: // \
129 | escapeString = "'";
130 | break;
131 | case 38: // &
132 | escapeString = "&";
133 | break;
134 | case 60: // <
135 | escapeString = "<";
136 | break;
137 | case 62: // >
138 | escapeString = ">";
139 | break;
140 | default:
141 | continue;
142 | }
143 | if (start) {
144 | result += text.slice(start, i);
145 | } else {
146 | result = text.slice(start, i);
147 | }
148 | result += escapeString;
149 | start = i + 1;
150 | }
151 | if (start && i !== start) {
152 | return result + text.slice(start, i);
153 | }
154 | return result;
155 | }
156 |
157 | export function isString(o) {
158 | return typeof o === 'string';
159 | }
160 |
161 | export function isNumber(o) {
162 | return typeof o === 'number';
163 | }
164 |
165 | export function renderStylesToString(styles) {
166 | if (isStringOrNumber(styles)) {
167 | return styles;
168 | } else {
169 | let renderedString = "";
170 | for (let styleName in styles) {
171 | const value = styles[styleName];
172 |
173 | if (isStringOrNumber(value)) {
174 | renderedString += `${kebabCase(styleName)}:${value};`;
175 | }
176 | }
177 | return renderedString;
178 | }
179 | }
180 |
181 | export function renderDatasetToString(dataset) {
182 | let renderedString = '';
183 | for (let key in dataset) {
184 | const dataKey = `data-${kebabCase(key)}`;
185 | const value = dataset[key];
186 | if (isString(value)) {
187 | renderedString += ` ${dataKey}="${escapeText(value)}"`;
188 | } else if (isNumber(value)) {
189 | renderedString += ` ${dataKey}="${value}"`;
190 | } else if (value === true) {
191 | renderedString += ` ${dataKey}="true"`;
192 | }
193 | }
194 | return renderedString;
195 | }
196 |
197 | export function renderAttributesToString(attributes) {
198 | let renderedString = '';
199 | for (let key in attributes) {
200 | renderedString += renderAttributeToString(key, attributes[key]);
201 | }
202 | return renderedString;
203 | }
204 |
205 | function renderAttributeToString(key, value) {
206 | if (isString(value)) {
207 | return ` ${key}="${escapeText(value)}"`;
208 | } else if (isNumber(value)) {
209 | return ` ${key}="${value}"`;
210 | } else if (value === true) {
211 | return ` ${key}`;
212 | } else {
213 | return '';
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const toString = Object.prototype.toString;
2 |
3 | export const doc = typeof document === 'undefined' ? {} : document;
4 |
5 | export const isArray = Array.isArray || function(arr) {
6 | return toString.call(arr) === '[object Array]';
7 | };
8 |
9 | export function isObject(o) {
10 | return typeof o === 'object' && o !== null;
11 | }
12 |
13 | export function isStringOrNumber(o) {
14 | const type = typeof o;
15 | return type === 'string' || type === 'number';
16 | }
17 |
18 | export function isNullOrUndefined(o) {
19 | return o === null || o === undefined;
20 | }
21 |
22 | export function isComponentInstance(o) {
23 | return o && typeof o.init === 'function';
24 | }
25 |
26 | export function isEventProp(propName) {
27 | return propName.substr(0, 3) === 'ev-';
28 | }
29 |
30 | export function isInvalid(o) {
31 | return isNullOrUndefined(o) || o === false || o === true;
32 | }
33 |
34 | export const indexOf = (function() {
35 | if (Array.prototype.indexOf) {
36 | return function(arr, value) {
37 | return arr.indexOf(value);
38 | };
39 | } else {
40 | return function(arr, value) {
41 | for (let i = 0; i < arr.length; i++) {
42 | if (arr[i] === value) {
43 | return i;
44 | }
45 | }
46 | return -1;
47 | };
48 | }
49 | })();
50 |
51 | const nativeObject = Object.create;
52 | export const createObject = (function() {
53 | if (nativeObject) {
54 | return function(obj) {
55 | return nativeObject(obj);
56 | };
57 | } else {
58 | return function(obj) {
59 | function Fn() {}
60 | Fn.prototype = obj;
61 | return new Fn();
62 | };
63 | }
64 | })();
65 |
66 | export const SimpleMap = typeof Map === 'function' ? Map : (function() {
67 | function SimpleMap() {
68 | this._keys = [];
69 | this._values = [];
70 | this.size = 0;
71 | }
72 |
73 | SimpleMap.prototype.set = function(key, value) {
74 | let index = indexOf(this._keys, key);
75 | if (!~index) {
76 | index = this._keys.push(key) - 1;
77 | this.size++;
78 | }
79 | this._values[index] = value;
80 | return this;
81 | };
82 | SimpleMap.prototype.get = function(key) {
83 | let index = indexOf(this._keys, key);
84 | if (!~index) return;
85 | return this._values[index];
86 | };
87 | SimpleMap.prototype.delete = function(key) {
88 | const index = indexOf(this._keys, key);
89 | if (!~index) return false;
90 | this._keys.splice(index, 1);
91 | this._values.splice(index, 1);
92 | this.size--;
93 | return true;
94 | };
95 |
96 | return SimpleMap;
97 | })();
98 |
99 | export const skipProps = {
100 | key: true,
101 | ref: true,
102 | children: true,
103 | className: true,
104 | checked: true,
105 | multiple: true,
106 | defaultValue: true,
107 | 'v-model': true,
108 | };
109 |
110 | export function isSkipProp(prop) {
111 | // treat prop which start with '_' as private prop, so skip it
112 | return skipProps[prop] || prop[0] === '_';
113 | }
114 |
115 | export const booleanProps = {
116 | muted: true,
117 | scoped: true,
118 | loop: true,
119 | open: true,
120 | checked: true,
121 | default: true,
122 | capture: true,
123 | disabled: true,
124 | readOnly: true,
125 | required: true,
126 | autoplay: true,
127 | controls: true,
128 | seamless: true,
129 | reversed: true,
130 | allowfullscreen: true,
131 | noValidate: true,
132 | hidden: true,
133 | autofocus: true,
134 | selected: true,
135 | indeterminate: true,
136 | multiple: true,
137 | };
138 |
139 | export const strictProps = {
140 | volume: true,
141 | defaultChecked: true,
142 | value: true,
143 | htmlFor: true,
144 | scrollLeft: true,
145 | scrollTop: true,
146 | };
147 |
148 | export const selfClosingTags = {
149 | 'area': true,
150 | 'base': true,
151 | 'br': true,
152 | 'col': true,
153 | 'command': true,
154 | 'embed': true,
155 | 'hr': true,
156 | 'img': true,
157 | 'input': true,
158 | 'keygen': true,
159 | 'link': true,
160 | 'menuitem': true,
161 | 'meta': true,
162 | 'param': true,
163 | 'source': true,
164 | 'track': true,
165 | 'wbr': true
166 | };
167 |
168 | export function MountedQueue() {
169 | this.queue = [];
170 | // if done is true, it indicate that this queue should be discarded
171 | this.done = false;
172 | }
173 | MountedQueue.prototype.push = function(fn) {
174 | this.queue.push(fn);
175 | };
176 | MountedQueue.prototype.unshift = function(fn) {
177 | this.queue.unshift(fn);
178 | };
179 | MountedQueue.prototype.trigger = function() {
180 | const queue = this.queue;
181 | let callback;
182 | while (callback = queue.shift()) {
183 | callback();
184 | }
185 | this.done = true;
186 | };
187 |
188 | export const browser = {};
189 | if (typeof navigator !== 'undefined') {
190 | const ua = navigator.userAgent.toLowerCase();
191 | const index = ua.indexOf('msie ');
192 | if (~index) {
193 | browser.isIE = true;
194 | const version = parseInt(ua.substring(index + 5, ua.indexOf('.', index)), 10);
195 | browser.version = version;
196 | browser.isIE8 = version === 8;
197 | } else if (~ua.indexOf('trident/')) {
198 | browser.isIE = true;
199 | const rv = ua.indexOf('rv:');
200 | browser.version = parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
201 | } else if (~ua.indexOf('edge')) {
202 | browser.isEdge = true;
203 | } else if (~ua.indexOf('safari')) {
204 | if (~ua.indexOf('chrome')) {
205 | browser.isChrome = true;
206 | } else {
207 | browser.isSafari = true;
208 | }
209 | }
210 | }
211 |
212 | export const setTextContent = browser.isIE8 ? function(dom, text) {
213 | dom.innerText = text;
214 | } : function(dom, text) {
215 | dom.textContent = text;
216 | };
217 |
218 | export const svgNS = "http://www.w3.org/2000/svg";
219 | export const xlinkNS = "http://www.w3.org/1999/xlink";
220 | export const xmlNS = "http://www.w3.org/XML/1998/namespace";
221 |
222 | export const namespaces = {
223 | 'xlink:href': xlinkNS,
224 | 'xlink:arcrole': xlinkNS,
225 | 'xlink:actuate': xlinkNS,
226 | 'xlink:show': xlinkNS,
227 | 'xlink:role': xlinkNS,
228 | 'xlink:title': xlinkNS,
229 | 'xlink:type': xlinkNS,
230 | 'xml:base': xmlNS,
231 | 'xml:lang': xmlNS,
232 | 'xml:space': xmlNS,
233 | };
234 |
235 | export const hooks = {
236 | beforeInsert: null
237 | };
238 |
239 | export const config = {
240 | disableDelegate: false, // for using in React/Vue, disable delegate the event
241 | delegateTarget: doc
242 | };
243 |
--------------------------------------------------------------------------------
/src/vdom.js:
--------------------------------------------------------------------------------
1 | import {Types, createTextVNode, EMPTY_OBJ, directClone} from './vnode';
2 | import {patchProps} from './vpatch';
3 | import {handleEvent} from './event';
4 | import {
5 | MountedQueue, isArray, isStringOrNumber,
6 | isNullOrUndefined, isEventProp, doc as document,
7 | setTextContent, svgNS, hooks
8 | } from './utils';
9 | import {processForm} from './wrappers/process';
10 |
11 | export function render(vNode, parentDom, mountedQueue, parentVNode, isSVG) {
12 | if (isNullOrUndefined(vNode)) return;
13 | let isTrigger = true;
14 | if (mountedQueue) {
15 | isTrigger = false;
16 | } else {
17 | mountedQueue = new MountedQueue();
18 | }
19 | const dom = createElement(vNode, parentDom, mountedQueue, true /* isRender */, parentVNode, isSVG);
20 | if (isTrigger) {
21 | mountedQueue.trigger();
22 | }
23 | return dom;
24 | }
25 |
26 | export function createElement(vNode, parentDom, mountedQueue, isRender, parentVNode, isSVG) {
27 | const type = vNode.type;
28 | if (type & Types.Element) {
29 | return createHtmlElement(vNode, parentDom, mountedQueue, isRender, parentVNode, isSVG);
30 | } else if (type & Types.Text) {
31 | return createTextElement(vNode, parentDom);
32 | } else if (type & Types.ComponentClassOrInstance) {
33 | return createComponentClassOrInstance(vNode, parentDom, mountedQueue, null, isRender, parentVNode, isSVG);
34 | // } else if (type & Types.ComponentFunction) {
35 | // return createComponentFunction(vNode, parentDom, mountedQueue, isNotAppendChild, isRender);
36 | // } else if (type & Types.ComponentInstance) {
37 | // return createComponentInstance(vNode, parentDom, mountedQueue);
38 | } else if (type & Types.HtmlComment) {
39 | return createCommentElement(vNode, parentDom);
40 | } else {
41 | throw new Error(`expect a vNode but got ${vNode}`);
42 | }
43 | }
44 |
45 | export function createHtmlElement(vNode, parentDom, mountedQueue, isRender, parentVNode, isSVG) {
46 | const type = vNode.type;
47 |
48 | isSVG = isSVG || (type & Types.SvgElement) > 0;
49 |
50 | const dom = documentCreateElement(vNode.tag, isSVG);
51 | const children = vNode.children;
52 | const props = vNode.props;
53 | const className = vNode.className;
54 |
55 | vNode.dom = dom;
56 | vNode.parentVNode = parentVNode;
57 |
58 | if (!isNullOrUndefined(children)) {
59 | createElements(children, dom, mountedQueue, isRender, vNode,
60 | isSVG === true && vNode.tag !== 'foreignObject'
61 | );
62 | }
63 |
64 | if (!isNullOrUndefined(className)) {
65 | if (isSVG) {
66 | dom.setAttribute('class', className);
67 | } else {
68 | dom.className = className;
69 | }
70 | }
71 |
72 | if (hooks.beforeInsert) {
73 | hooks.beforeInsert(vNode);
74 | }
75 |
76 | // in IE8, the select value will be set to the first option's value forcely
77 | // when it is appended to parent dom. We change its value in processForm does not
78 | // work. So processForm after it has be appended to parent dom.
79 | if (parentDom) {
80 | parentDom.appendChild(dom);
81 | }
82 | if (props !== EMPTY_OBJ) {
83 | patchProps(null, vNode, isSVG, true);
84 | }
85 |
86 | const ref = vNode.ref;
87 | if (!isNullOrUndefined(ref)) {
88 | createRef(dom, ref, mountedQueue);
89 | }
90 |
91 | return dom;
92 | }
93 |
94 | export function createTextElement(vNode, parentDom) {
95 | const dom = document.createTextNode(vNode.children);
96 | vNode.dom = dom;
97 |
98 | if (parentDom) {
99 | parentDom.appendChild(dom);
100 | }
101 |
102 | return dom;
103 | }
104 |
105 | export function createOrHydrateComponentClassOrInstance(vNode, parentDom, mountedQueue, lastVNode, isRender, parentVNode, isSVG, createDom) {
106 | const props = vNode.props;
107 | const instance = vNode.type & Types.ComponentClass ?
108 | new vNode.tag(props) : vNode.children;
109 | instance.parentDom = parentDom;
110 | instance.mountedQueue = mountedQueue;
111 | instance.isRender = isRender;
112 | instance.parentVNode = parentVNode;
113 | instance.isSVG = isSVG;
114 | instance.vNode = vNode;
115 | vNode.children = instance;
116 | vNode.parentVNode = parentVNode;
117 |
118 | const dom = createDom(instance);
119 | const ref = vNode.ref;
120 |
121 | vNode.dom = dom;
122 |
123 | if (typeof instance.mount === 'function') {
124 | mountedQueue.push(() => instance.mount(lastVNode, vNode));
125 | }
126 |
127 | if (typeof ref === 'function') {
128 | ref(instance);
129 | }
130 |
131 | return dom;
132 | }
133 |
134 | export function createComponentClassOrInstance(vNode, parentDom, mountedQueue, lastVNode, isRender, parentVNode, isSVG) {
135 | return createOrHydrateComponentClassOrInstance(vNode, parentDom, mountedQueue, lastVNode, isRender, parentVNode, isSVG, (instance) => {
136 | const dom = instance.init(lastVNode, vNode);
137 | if (parentDom) {
138 | // for Animate component reuse dom in Intact
139 | if (!lastVNode && parentDom._reserve) {
140 | lastVNode = parentDom._reserve[vNode.key];
141 | }
142 | if (
143 | !lastVNode ||
144 | // maybe we have reused the component and replaced the dom
145 | lastVNode.dom !== dom && !dom.parentNode || !dom.parentNode.tagName
146 | ) {
147 | parentDom.appendChild(dom);
148 | }
149 | }
150 |
151 | return dom;
152 | });
153 | }
154 |
155 | // export function createComponentFunction(vNode, parentDom, mountedQueue) {
156 | // const props = vNode.props;
157 | // const ref = vNode.ref;
158 |
159 | // createComponentFunctionVNode(vNode);
160 |
161 | // let children = vNode.children;
162 | // let dom;
163 | // // support ComponentFunction return an array for macro usage
164 | // if (isArray(children)) {
165 | // dom = [];
166 | // for (let i = 0; i < children.length; i++) {
167 | // dom.push(createElement(children[i], parentDom, mountedQueue));
168 | // }
169 | // } else {
170 | // dom = createElement(vNode.children, parentDom, mountedQueue);
171 | // }
172 | // vNode.dom = dom;
173 |
174 | // // if (parentDom) {
175 | // // parentDom.appendChild(dom);
176 | // // }
177 |
178 | // if (ref) {
179 | // createRef(dom, ref, mountedQueue);
180 | // }
181 |
182 | // return dom;
183 | // }
184 |
185 | export function createCommentElement(vNode, parentDom) {
186 | const dom = document.createComment(vNode.children);
187 | vNode.dom = dom;
188 |
189 | if (parentDom) {
190 | parentDom.appendChild(dom);
191 | }
192 |
193 | return dom;
194 | }
195 |
196 | // export function createComponentFunctionVNode(vNode) {
197 | // let result = vNode.tag(vNode.props);
198 | // if (isStringOrNumber(result)) {
199 | // result = createTextVNode(result);
200 | // } else if (process.env.NODE_ENV !== 'production') {
201 | // if (isArray(result)) {
202 | // throw new Error(`ComponentFunction ${vNode.tag.name} returned a invalid vNode`);
203 | // }
204 | // }
205 |
206 | // vNode.children = result;
207 |
208 | // return vNode;
209 | // }
210 |
211 | export function createElements(vNodes, parentDom, mountedQueue, isRender, parentVNode, isSVG) {
212 | if (isStringOrNumber(vNodes)) {
213 | setTextContent(parentDom, vNodes);
214 | } else if (isArray(vNodes)) {
215 | let cloned = false;
216 | for (let i = 0; i < vNodes.length; i++) {
217 | let child = vNodes[i];
218 | if (child.dom) {
219 | if (!cloned) {
220 | parentVNode.children = vNodes = vNodes.slice(0);
221 | cloned = true;
222 | }
223 | vNodes[i] = child = directClone(child);
224 | }
225 | createElement(child, parentDom, mountedQueue, isRender, parentVNode, isSVG);
226 | }
227 | } else {
228 | if (vNodes.dom) {
229 | parentVNode.children = vNodes = directClone(vNodes);
230 | }
231 | createElement(vNodes, parentDom, mountedQueue, isRender, parentVNode, isSVG);
232 | }
233 | }
234 |
235 | export function removeElements(vNodes, parentDom) {
236 | if (isNullOrUndefined(vNodes)) {
237 | return;
238 | } else if (isArray(vNodes)) {
239 | for (let i = 0; i < vNodes.length; i++) {
240 | removeElement(vNodes[i], parentDom);
241 | }
242 | } else {
243 | removeElement(vNodes, parentDom);
244 | }
245 | }
246 |
247 | export function removeElement(vNode, parentDom, nextVNode) {
248 | const type = vNode.type;
249 | if (type & Types.Element) {
250 | return removeHtmlElement(vNode, parentDom);
251 | } else if (type & Types.TextElement) {
252 | return removeText(vNode, parentDom);
253 | } else if (type & Types.ComponentClassOrInstance) {
254 | return removeComponentClassOrInstance(vNode, parentDom, nextVNode);
255 | } else if (type & Types.ComponentFunction) {
256 | return removeComponentFunction(vNode, parentDom);
257 | }
258 | }
259 |
260 | export function removeHtmlElement(vNode, parentDom) {
261 | const ref = vNode.ref;
262 | const props = vNode.props;
263 | const dom = vNode.dom;
264 |
265 | if (ref) {
266 | ref(null);
267 | }
268 |
269 | removeElements(vNode.children, null);
270 |
271 | // remove event
272 | for (let name in props) {
273 | const prop = props[name];
274 | if (!isNullOrUndefined(prop) && isEventProp(name)) {
275 | handleEvent(name.substr(3), prop, null, dom);
276 | }
277 | }
278 |
279 | if (parentDom) {
280 | parentDom.removeChild(dom);
281 | }
282 | }
283 |
284 | export function removeText(vNode, parentDom) {
285 | if (parentDom) {
286 | parentDom.removeChild(vNode.dom);
287 | }
288 | }
289 |
290 | export function removeComponentFunction(vNode, parentDom) {
291 | const ref = vNode.ref;
292 | if (ref) {
293 | ref(null);
294 | }
295 | removeElement(vNode.children, parentDom);
296 | }
297 |
298 | export function removeComponentClassOrInstance(vNode, parentDom, nextVNode) {
299 | const instance = vNode.children;
300 | const ref = vNode.ref;
301 |
302 | if (typeof instance.destroy === 'function') {
303 | instance._isRemoveDirectly = !!parentDom;
304 | instance.destroy(vNode, nextVNode, parentDom);
305 | }
306 |
307 | if (ref) {
308 | ref(null);
309 | }
310 |
311 | // instance destroy method will remove everything
312 | // removeElements(vNode.props.children, null);
313 |
314 | if (parentDom) {
315 | removeChild(parentDom, vNode);
316 | }
317 | }
318 |
319 | export function removeAllChildren(dom, vNodes) {
320 | // setTextContent(dom, '');
321 | // removeElements(vNodes);
322 | }
323 |
324 | export function replaceChild(parentDom, lastVNode, nextVNode) {
325 | const lastDom = lastVNode.dom;
326 | const nextDom = nextVNode.dom;
327 | const parentNode = lastDom.parentNode;
328 | // maybe the lastDom has be moved
329 | if (!parentDom || parentNode !== parentDom) parentDom = parentNode;
330 | if (lastDom._unmount) {
331 | lastDom._unmount(lastVNode, parentDom);
332 | if (!nextDom.parentNode) {
333 | if (parentDom.lastChild === lastDom) {
334 | parentDom.appendChild(nextDom);
335 | } else {
336 | parentDom.insertBefore(nextDom, lastDom.nextSibling);
337 | }
338 | }
339 | } else {
340 | parentDom.replaceChild(nextDom, lastDom);
341 | }
342 | }
343 |
344 | export function removeChild(parentDom, vNode) {
345 | const dom = vNode.dom;
346 | if (dom._unmount) {
347 | dom._unmount(vNode, parentDom);
348 | } else {
349 | parentDom.removeChild(dom);
350 | }
351 | }
352 |
353 | export function createRef(dom, ref, mountedQueue) {
354 | if (typeof ref === 'function') {
355 | // mountedQueue.push(() => ref(dom));
356 | // set ref immediately, because we have unset it before
357 | ref(dom);
358 | } else {
359 | throw new Error(`ref must be a function, but got "${JSON.stringify(ref)}"`);
360 | }
361 | }
362 |
363 | export function documentCreateElement(tag, isSVG) {
364 | if (isSVG === true) {
365 | return document.createElementNS(svgNS, tag);
366 | } else {
367 | return document.createElement(tag);
368 | }
369 | }
370 |
--------------------------------------------------------------------------------
/src/vnode.js:
--------------------------------------------------------------------------------
1 | import {
2 | isArray, isStringOrNumber, isNullOrUndefined,
3 | isComponentInstance, browser, isInvalid
4 | } from './utils';
5 |
6 | export const Types = {
7 | Text: 1,
8 | HtmlElement: 1 << 1,
9 |
10 | ComponentClass: 1 << 2,
11 | ComponentFunction: 1 << 3,
12 | ComponentInstance: 1 << 4,
13 |
14 | HtmlComment: 1 << 5,
15 |
16 | InputElement: 1 << 6,
17 | SelectElement: 1 << 7,
18 | TextareaElement: 1 << 8,
19 | SvgElement: 1 << 9,
20 |
21 | UnescapeText: 1 << 10 // for server side render unescape text
22 | };
23 | Types.FormElement = Types.InputElement | Types.SelectElement | Types.TextareaElement;
24 | Types.Element = Types.HtmlElement | Types.FormElement | Types.SvgElement;
25 | Types.ComponentClassOrInstance = Types.ComponentClass | Types.ComponentInstance;
26 | Types.TextElement = Types.Text | Types.HtmlComment;
27 |
28 | export const EMPTY_OBJ = {};
29 | if (process.env.NODE_ENV !== 'production' && !browser.isIE) {
30 | Object.freeze(EMPTY_OBJ);
31 | }
32 |
33 | export function VNode(type, tag, props, children, className, key, ref) {
34 | this.type = type;
35 | this.tag = tag;
36 | this.props = props;
37 | this.children = children;
38 | this.key = key;
39 | this.ref = ref;
40 | this.className = className;
41 | }
42 |
43 | export function createVNode(tag, props, children, className, key, ref) {
44 | let type;
45 | props || (props = EMPTY_OBJ);
46 | switch (typeof tag) {
47 | case 'string':
48 | if (tag === 'input') {
49 | type = Types.InputElement;
50 | } else if(tag === 'select') {
51 | type = Types.SelectElement;
52 | } else if (tag === 'textarea') {
53 | type = Types.TextareaElement;
54 | } else if (tag === 'svg') {
55 | type = Types.SvgElement;
56 | } else {
57 | type = Types.HtmlElement;
58 | }
59 | break;
60 | case 'function':
61 | // arrow function has not prototype
62 | if (tag.prototype && tag.prototype.init) {
63 | type = Types.ComponentClass;
64 | } else {
65 | // return tag(props);
66 | type = Types.ComponentFunction;
67 | }
68 | break;
69 | case 'object':
70 | if (tag.init) {
71 | return createComponentInstanceVNode(tag);
72 | }
73 | default:
74 | throw new Error(`unknown vNode type: ${tag}`);
75 | }
76 |
77 | if (type & (Types.ComponentClass | Types.ComponentFunction)) {
78 | if (!isNullOrUndefined(children)) {
79 | if (props === EMPTY_OBJ) props = {};
80 | props.children = normalizeChildren(children, false);
81 | } else if (!isNullOrUndefined(props.children)) {
82 | props.children = normalizeChildren(props.children, false);
83 | }
84 | if (type & Types.ComponentFunction) {
85 | if (key || ref) {
86 | if (props === EMPTY_OBJ) props = {};
87 | if (key) props.key = key;
88 | if (ref) props.ref = ref;
89 | }
90 | return tag(props);
91 | }
92 | } else if (!isNullOrUndefined(children)) {
93 | children = normalizeChildren(children, true);
94 | }
95 |
96 | return new VNode(type, tag, props, children,
97 | className || props.className,
98 | key || props.key,
99 | ref || props.ref
100 | );
101 | }
102 |
103 | export function createCommentVNode(children, key) {
104 | return new VNode(Types.HtmlComment, null, EMPTY_OBJ, children, null, key);
105 | }
106 |
107 | export function createUnescapeTextVNode(children) {
108 | return new VNode(Types.UnescapeText, null, EMPTY_OBJ, children);
109 | }
110 |
111 | export function createTextVNode(text, key) {
112 | return new VNode(Types.Text, null, EMPTY_OBJ, text, null, key);
113 | }
114 |
115 | export function createVoidVNode() {
116 | return new VNode(Types.VoidElement, null, EMPTY_OBJ);
117 | }
118 |
119 | export function createComponentInstanceVNode(instance) {
120 | const props = instance.props || EMPTY_OBJ;
121 | return new VNode(Types.ComponentInstance, instance.constructor,
122 | props, instance, null, props.key, props.ref
123 | );
124 | }
125 |
126 | function normalizeChildren(vNodes, isAddKey) {
127 | if (isArray(vNodes)) {
128 | const childNodes = addChild(vNodes, {index: 0}, isAddKey);
129 | return childNodes.length ? childNodes : null;
130 | } else if (isComponentInstance(vNodes)) {
131 | return createComponentInstanceVNode(vNodes);
132 | } else if (vNodes.type && isAddKey) {
133 | if (!isNullOrUndefined(vNodes.dom) || vNodes.$) {
134 | return directClone(vNodes);
135 | }
136 |
137 | // add a flag to indicate that we have handle the vNode
138 | // when it came back we should clone it
139 | vNodes.$ = true;
140 | }
141 | return vNodes;
142 | }
143 |
144 | function applyKey(vNode, reference, isAddKey) {
145 | if (!isAddKey) return vNode;
146 | // start with '.' means the vNode has been set key by index
147 | // we will reset the key when it comes back again
148 | if (isNullOrUndefined(vNode.key) || vNode.key[0] === '.') {
149 | vNode.key = `.$${reference.index++}`;
150 | }
151 | // add a flag to indicate that we have handle the vNode
152 | // when it came back we should clone it
153 | vNode.$ = true;
154 | return vNode;
155 | }
156 |
157 | function addChild(vNodes, reference, isAddKey) {
158 | let newVNodes;
159 | for (let i = 0; i < vNodes.length; i++) {
160 | const n = vNodes[i];
161 | if (isNullOrUndefined(n)) {
162 | if (!newVNodes) {
163 | newVNodes = vNodes.slice(0, i);
164 | }
165 | } else if (isArray(n)) {
166 | if (!newVNodes) {
167 | newVNodes = vNodes.slice(0, i);
168 | }
169 | newVNodes = newVNodes.concat(addChild(n, reference, isAddKey));
170 | } else if (isStringOrNumber(n)) {
171 | if (!newVNodes) {
172 | newVNodes = vNodes.slice(0, i);
173 | }
174 | newVNodes.push(applyKey(createTextVNode(n), reference, isAddKey));
175 | } else if (isComponentInstance(n)) {
176 | if (!newVNodes) {
177 | newVNodes = vNodes.slice(0, i);
178 | }
179 | newVNodes.push(applyKey(createComponentInstanceVNode(n), reference, isAddKey));
180 | } else if (n.type) {
181 | if (!newVNodes) {
182 | newVNodes = vNodes.slice(0, i);
183 | }
184 | if (isAddKey && (n.dom || n.$)) {
185 | newVNodes.push(applyKey(directClone(n), reference, isAddKey));
186 | } else {
187 | newVNodes.push(applyKey(n, reference, isAddKey));
188 | }
189 | }
190 | }
191 | return newVNodes || vNodes;
192 | }
193 |
194 | export function directClone(vNode, extraProps, changeType) {
195 | let newVNode;
196 | const type = vNode.type;
197 |
198 | if (type & (Types.ComponentClassOrInstance | Types.Element)) {
199 | // maybe we does not shadow copy props
200 | let props = vNode.props || EMPTY_OBJ;
201 | /**
202 | * if this is a instance vNode, then we must change its type to new instance again
203 | *
204 | * but if we change the type, it will lead to replace element because of different type.
205 | * only change the type, when we really clone it
206 | */
207 | let _type = (type & Types.ComponentInstance) && changeType ? Types.ComponentClass : type;
208 | if (extraProps) {
209 | // if exist extraProps, shadow copy
210 | let _props = {};
211 | for (let key in props) {
212 | _props[key] = props[key];
213 | }
214 | for (let key in extraProps) {
215 | _props[key] = extraProps[key];
216 | }
217 | const children = extraProps.children;
218 | if (children) {
219 | _props.children = normalizeChildren(children, false);
220 | }
221 |
222 | newVNode = new VNode(
223 | _type, vNode.tag, _props,
224 | vNode.children,
225 | _props.className || vNode.className,
226 | _props.key || vNode.key,
227 | _props.ref || vNode.ref
228 | );
229 | } else {
230 | newVNode = new VNode(
231 | _type, vNode.tag, props,
232 | vNode.children,
233 | vNode.className,
234 | vNode.key,
235 | vNode.ref
236 | );
237 | }
238 | } else if (type & Types.Text) {
239 | newVNode = createTextVNode(vNode.children, vNode.key);
240 | } else if (type & Types.HtmlComment) {
241 | newVNode = createCommentVNode(vNode.children, vNode.key);
242 | }
243 |
244 | return newVNode;
245 | }
246 |
247 | function directCloneChildren(children) {
248 | if (children) {
249 | if (isArray(children)) {
250 | const len = children.length;
251 | if (len > 0) {
252 | const tmpArray = [];
253 |
254 | for (let i = 0; i < len; i++) {
255 | const child = children[i];
256 | if (isStringOrNumber(child)) {
257 | tmpArray.push(child);
258 | } else if (!isInvalid(child) && child.type) {
259 | tmpArray.push(directClone(child));
260 | }
261 | }
262 | return tmpArray;
263 | }
264 | } else if (children.type) {
265 | return directClone(children);
266 | }
267 | }
268 |
269 | return children;
270 | }
271 |
--------------------------------------------------------------------------------
/src/vpatch.js:
--------------------------------------------------------------------------------
1 | import {Types, EMPTY_OBJ} from './vnode';
2 | import {
3 | createElement,
4 | createElements,
5 | removeElements,
6 | removeElement,
7 | removeComponentClassOrInstance,
8 | removeAllChildren,
9 | createComponentClassOrInstance,
10 | // createComponentFunction,
11 | // createComponentFunctionVNode,
12 | createRef,
13 | replaceChild
14 | } from './vdom';
15 | import {isObject, isArray, isNullOrUndefined,
16 | isSkipProp, MountedQueue, isEventProp,
17 | booleanProps, strictProps,
18 | browser, setTextContent, isStringOrNumber,
19 | namespaces, SimpleMap
20 | } from './utils';
21 | import {handleEvent} from './event';
22 | import {processForm} from './wrappers/process';
23 |
24 | export function patch(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG) {
25 | let isTrigger = true;
26 | if (mountedQueue) {
27 | isTrigger = false;
28 | } else {
29 | mountedQueue = new MountedQueue();
30 | }
31 | const dom = patchVNode(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
32 | if (isTrigger) {
33 | mountedQueue.trigger();
34 | }
35 | return dom;
36 | }
37 |
38 | export function patchVNode(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG) {
39 | const nextType = nextVNode.type;
40 | const lastType = lastVNode.type;
41 |
42 | if (nextType & Types.Element) {
43 | if (lastType & Types.Element) {
44 | patchElement(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
45 | } else {
46 | replaceElement(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
47 | }
48 | } else if (nextType & Types.TextElement) {
49 | if (lastType === nextType) {
50 | patchText(lastVNode, nextVNode);
51 | } else {
52 | replaceElement(lastVNode, nextVNode, parentDom, mountedQueue, isSVG);
53 | }
54 | } else if (nextType & Types.ComponentClass) {
55 | if (lastType & Types.ComponentClass) {
56 | patchComponentClass(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
57 | } else {
58 | replaceElement(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
59 | }
60 | // } else if (nextType & Types.ComponentFunction) {
61 | // if (lastType & Types.ComponentFunction) {
62 | // patchComponentFunction(lastVNode, nextVNode, parentDom, mountedQueue);
63 | // } else {
64 | // replaceElement(lastVNode, nextVNode, parentDom, mountedQueue);
65 | // }
66 | } else if (nextType & Types.ComponentInstance) {
67 | if (lastType & Types.ComponentInstance) {
68 | patchComponentInstance(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
69 | } else {
70 | replaceElement(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
71 | }
72 | }
73 |
74 | return nextVNode.dom;
75 | }
76 |
77 | function patchElement(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG) {
78 | const dom = lastVNode.dom;
79 | const lastProps = lastVNode.props;
80 | const nextProps = nextVNode.props;
81 | const lastChildren = lastVNode.children;
82 | const nextChildren = nextVNode.children;
83 | const lastClassName = lastVNode.className;
84 | const nextClassName = nextVNode.className;
85 | const nextType = nextVNode.type;
86 |
87 | nextVNode.dom = dom;
88 | nextVNode.parentVNode = parentVNode;
89 |
90 | isSVG = isSVG || (nextType & Types.SvgElement) > 0
91 |
92 | if (lastVNode.tag !== nextVNode.tag || lastVNode.key !== nextVNode.key) {
93 | replaceElement(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG);
94 | } else {
95 | if (lastChildren !== nextChildren) {
96 | patchChildren(lastChildren, nextChildren, dom, mountedQueue, nextVNode,
97 | isSVG === true && nextVNode.tag !== 'foreignObject'
98 | );
99 | }
100 |
101 | if (lastProps !== nextProps) {
102 | patchProps(lastVNode, nextVNode, isSVG, false);
103 | }
104 |
105 | if (lastClassName !== nextClassName) {
106 | if (isNullOrUndefined(nextClassName)) {
107 | dom.removeAttribute('class');
108 | } else {
109 | if (isSVG) {
110 | dom.setAttribute('class', nextClassName);
111 | } else {
112 | dom.className = nextClassName;
113 | }
114 | }
115 | }
116 |
117 | const lastRef = lastVNode.ref;
118 | const nextRef = nextVNode.ref;
119 | if (lastRef !== nextRef) {
120 | if (!isNullOrUndefined(lastRef)) {
121 | lastRef(null);
122 | }
123 | if (!isNullOrUndefined(nextRef)) {
124 | createRef(dom, nextRef, mountedQueue);
125 | }
126 | }
127 | }
128 | }
129 |
130 | function patchComponentClass(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG) {
131 | const lastTag = lastVNode.tag;
132 | const nextTag = nextVNode.tag;
133 | const dom = lastVNode.dom;
134 |
135 | let instance;
136 | let newDom;
137 |
138 | if (lastTag !== nextTag || lastVNode.key !== nextVNode.key) {
139 | // we should call this remove function in component's init method
140 | // because it should be destroyed until async component has rendered
141 | // removeComponentClassOrInstance(lastVNode, null, nextVNode);
142 | newDom = createComponentClassOrInstance(nextVNode, parentDom, mountedQueue, lastVNode, false, parentVNode, isSVG);
143 | } else {
144 | instance = lastVNode.children;
145 | instance.mountedQueue = mountedQueue;
146 | if (instance.mounted) {
147 | instance.isRender = false;
148 | }
149 | instance.parentVNode = parentVNode;
150 | instance.vNode = nextVNode;
151 | instance.isSVG = isSVG;
152 | nextVNode.children = instance;
153 | nextVNode.parentVNode = parentVNode;
154 | newDom = instance.update(lastVNode, nextVNode);
155 | nextVNode.dom = newDom;
156 |
157 | // for intact.js, the dom will not be removed and
158 | // the component will not be destoryed, so the ref
159 | // function need be called in update method.
160 | const lastRef = lastVNode.ref;
161 | const nextRef = nextVNode.ref;
162 | if (lastRef !== nextRef) {
163 | if (!isNullOrUndefined(lastRef)) {
164 | lastRef(null);
165 | }
166 | if (!isNullOrUndefined(nextRef)) {
167 | nextRef(instance);
168 | }
169 | }
170 | }
171 |
172 | // perhaps the dom has be replaced
173 | if (dom !== newDom && dom.parentNode &&
174 | // when dom has be replaced, its parentNode maybe be fragment in IE8
175 | dom.parentNode.nodeName !== '#document-fragment'
176 | ) {
177 | replaceChild(parentDom, lastVNode, nextVNode);
178 | }
179 | }
180 |
181 | function patchComponentInstance(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG) {
182 | const lastInstance = lastVNode.children;
183 | const nextInstance = nextVNode.children;
184 | const dom = lastVNode.dom;
185 |
186 | let newDom;
187 |
188 | if (lastInstance !== nextInstance) {
189 | // removeComponentClassOrInstance(lastVNode, null, nextVNode);
190 | newDom = createComponentClassOrInstance(nextVNode, parentDom, mountedQueue, lastVNode, false, parentVNode, isSVG);
191 | } else {
192 | lastInstance.mountedQueue = mountedQueue;
193 | if (lastInstance.mounted) {
194 | lastInstance.isRender = false;
195 | }
196 | lastInstance.vNode = nextVNode;
197 | lastInstance.parentVNode = parentVNode;
198 | nextVNode.parentVNode = parentVNode;
199 | newDom = lastInstance.update(lastVNode, nextVNode);
200 | nextVNode.dom = newDom;
201 |
202 | const ref = nextVNode.ref;
203 | if (typeof ref === 'function') {
204 | ref(instance);
205 | }
206 | }
207 |
208 | if (dom !== newDom && dom.parentNode &&
209 | // when dom has be replaced, its parentNode maybe be fragment in IE8
210 | dom.parentNode.nodeName !== '#document-fragment'
211 | ) {
212 | replaceChild(parentDom, lastVNode, nextVNode);
213 | }
214 | }
215 |
216 | // function patchComponentFunction(lastVNode, nextVNode, parentDom, mountedQueue) {
217 | // const lastTag = lastVNode.tag;
218 | // const nextTag = nextVNode.tag;
219 |
220 | // if (lastVNode.key !== nextVNode.key) {
221 | // removeElements(lastVNode.children, parentDom);
222 | // createComponentFunction(nextVNode, parentDom, mountedQueue);
223 | // } else {
224 | // nextVNode.dom = lastVNode.dom;
225 | // createComponentFunctionVNode(nextVNode);
226 | // patchChildren(lastVNode.children, nextVNode.children, parentDom, mountedQueue);
227 | // }
228 | // }
229 |
230 | function patchChildren(lastChildren, nextChildren, parentDom, mountedQueue, parentVNode, isSVG) {
231 | if (isNullOrUndefined(lastChildren)) {
232 | if (!isNullOrUndefined(nextChildren)) {
233 | createElements(nextChildren, parentDom, mountedQueue, false, parentVNode, isSVG);
234 | }
235 | } else if (isNullOrUndefined(nextChildren)) {
236 | if (isStringOrNumber(lastChildren)) {
237 | setTextContent(parentDom, '');
238 | } else {
239 | removeElements(lastChildren, parentDom);
240 | }
241 | } else if (isStringOrNumber(nextChildren)) {
242 | if (isStringOrNumber(lastChildren)) {
243 | setTextContent(parentDom, nextChildren);
244 | } else {
245 | removeElements(lastChildren, parentDom);
246 | setTextContent(parentDom, nextChildren);
247 | }
248 | } else if (isArray(lastChildren)) {
249 | if (isArray(nextChildren)) {
250 | patchChildrenByKey(lastChildren, nextChildren, parentDom, mountedQueue, parentVNode, isSVG);
251 | } else {
252 | removeElements(lastChildren, parentDom);
253 | createElement(nextChildren, parentDom, mountedQueue, false, parentVNode, isSVG);
254 | }
255 | } else if (isArray(nextChildren)) {
256 | if (isStringOrNumber(lastChildren)) {
257 | setTextContent(parentDom, '');
258 | } else {
259 | removeElement(lastChildren, parentDom);
260 | }
261 | createElements(nextChildren, parentDom, mountedQueue, false, parentVNode, isSVG);
262 | } else if (isStringOrNumber(lastChildren)) {
263 | setTextContent(parentDom, '');
264 | createElement(nextChildren, parentDom, mountedQueue, false, parentVNode, isSVG);
265 | } else {
266 | patchVNode(lastChildren, nextChildren, parentDom, mountedQueue, parentVNode, isSVG);
267 | }
268 | }
269 |
270 | function patchChildrenByKey(a, b, dom, mountedQueue, parentVNode, isSVG) {
271 | let aLength = a.length;
272 | let bLength = b.length;
273 | let aEnd = aLength - 1;
274 | let bEnd = bLength - 1;
275 | let aStart = 0;
276 | let bStart = 0;
277 | let i;
278 | let j;
279 | let aNode;
280 | let bNode;
281 | let nextNode;
282 | let nextPos;
283 | let node;
284 | let aStartNode = a[aStart];
285 | let bStartNode = b[bStart];
286 | let aEndNode = a[aEnd];
287 | let bEndNode = b[bEnd];
288 |
289 | outer: while (true) {
290 | while (aStartNode.key === bStartNode.key) {
291 | patchVNode(aStartNode, bStartNode, dom, mountedQueue, parentVNode, isSVG);
292 | ++aStart;
293 | ++bStart;
294 | if (aStart > aEnd || bStart > bEnd) {
295 | break outer;
296 | }
297 | aStartNode = a[aStart];
298 | bStartNode = b[bStart];
299 | }
300 | while (aEndNode.key === bEndNode.key) {
301 | patchVNode(aEndNode, bEndNode, dom, mountedQueue, parentVNode, isSVG);
302 | --aEnd;
303 | --bEnd;
304 | if (aEnd < aStart || bEnd < bStart) {
305 | break outer;
306 | }
307 | aEndNode = a[aEnd];
308 | bEndNode = b[bEnd];
309 | }
310 |
311 | if (aEndNode.key === bStartNode.key) {
312 | patchVNode(aEndNode, bStartNode, dom, mountedQueue, parentVNode, isSVG);
313 | dom.insertBefore(bStartNode.dom, aStartNode.dom);
314 | --aEnd;
315 | ++bStart;
316 | aEndNode = a[aEnd];
317 | bStartNode = b[bStart];
318 | continue;
319 | }
320 |
321 | if (aStartNode.key === bEndNode.key) {
322 | patchVNode(aStartNode, bEndNode, dom, mountedQueue, parentVNode, isSVG);
323 | insertOrAppend(bEnd, bLength, bEndNode.dom, b, dom);
324 | ++aStart;
325 | --bEnd;
326 | aStartNode = a[aStart];
327 | bEndNode = b[bEnd];
328 | continue;
329 | }
330 | break;
331 | }
332 |
333 | if (aStart > aEnd) {
334 | while (bStart <= bEnd) {
335 | insertOrAppend(
336 | bEnd, bLength,
337 | createElement(b[bStart], null, mountedQueue, false, parentVNode, isSVG),
338 | b, dom, true /* detectParent: for animate, if the parentNode exists, then do nothing*/
339 | );
340 | ++bStart;
341 | }
342 | } else if (bStart > bEnd) {
343 | while (aStart <= aEnd) {
344 | removeElement(a[aStart], dom);
345 | ++aStart;
346 | }
347 | } else {
348 | aLength = aEnd - aStart + 1;
349 | bLength = bEnd - bStart + 1;
350 | const sources = new Array(bLength);
351 | for (i = 0; i < bLength; i++) {
352 | sources[i] = -1;
353 | }
354 | let moved = false;
355 | let pos = 0;
356 | let patched = 0;
357 |
358 | if (bLength <= 4 || aLength * bLength <= 16) {
359 | for (i = aStart; i <= aEnd; i++) {
360 | aNode = a[i];
361 | if (patched < bLength) {
362 | for (j = bStart; j <= bEnd; j++) {
363 | bNode = b[j];
364 | if (aNode.key === bNode.key) {
365 | sources[j - bStart] = i;
366 | if (pos > j) {
367 | moved = true;
368 | } else {
369 | pos = j;
370 | }
371 | patchVNode(aNode, bNode, dom, mountedQueue, parentVNode, isSVG);
372 | ++patched;
373 | a[i] = null;
374 | break;
375 | }
376 | }
377 | }
378 | }
379 | } else {
380 | var keyIndex = new SimpleMap();
381 | for (i = bStart; i <= bEnd; i++) {
382 | keyIndex.set(b[i].key, i);
383 | }
384 | for (i = aStart; i <= aEnd; i++) {
385 | aNode = a[i];
386 | if (patched < bLength) {
387 | j = keyIndex.get(aNode.key);
388 | if (j !== undefined) {
389 | bNode = b[j];
390 | sources[j - bStart] = i;
391 | if (pos > j) {
392 | moved = true;
393 | } else {
394 | pos = j;
395 | }
396 | patchVNode(aNode, bNode, dom, mountedQueue, parentVNode, isSVG);
397 | ++patched;
398 | a[i] = null;
399 | }
400 | }
401 | }
402 | }
403 | if (aLength === a.length && patched === 0) {
404 | // removeAllChildren(dom, a);
405 | // children maybe have animation
406 | removeElements(a, dom);
407 | while (bStart < bLength) {
408 | createElement(b[bStart], dom, mountedQueue, false, parentVNode, isSVG);
409 | ++bStart;
410 | }
411 | } else {
412 | // some browsers, e.g. ie, must insert before remove for some element,
413 | // e.g. select/option, otherwise the selected property will be weird
414 | if (moved) {
415 | const seq = lisAlgorithm(sources);
416 | j = seq.length - 1;
417 | for (i = bLength - 1; i >= 0; i--) {
418 | if (sources[i] === -1) {
419 | pos = i + bStart;
420 | insertOrAppend(
421 | pos, b.length,
422 | createElement(b[pos], null, mountedQueue, false, parentVNode, isSVG),
423 | b, dom
424 | );
425 | } else {
426 | if (j < 0 || i !== seq[j]) {
427 | pos = i + bStart;
428 | insertOrAppend(pos, b.length, b[pos].dom, b, dom);
429 | } else {
430 | --j;
431 | }
432 | }
433 | }
434 | } else if (patched !== bLength) {
435 | for (i = bLength - 1; i >= 0; i--) {
436 | if (sources[i] === -1) {
437 | pos = i + bStart;
438 | insertOrAppend(
439 | pos, b.length,
440 | createElement(b[pos], null, mountedQueue, false, parentVNode, isSVG),
441 | b, dom, true
442 | );
443 | }
444 | }
445 | }
446 | i = aLength - patched;
447 | while (i > 0) {
448 | aNode = a[aStart++];
449 | if (aNode !== null) {
450 | removeElement(aNode, dom);
451 | --i;
452 | }
453 | }
454 | }
455 | }
456 | }
457 |
458 | function lisAlgorithm(arr) {
459 | let p = arr.slice(0);
460 | let result = [0];
461 | let i;
462 | let j;
463 | let u;
464 | let v;
465 | let c;
466 | let len = arr.length;
467 | for (i = 0; i < len; i++) {
468 | let arrI = arr[i];
469 | if (arrI === -1) {
470 | continue;
471 | }
472 | j = result[result.length - 1];
473 | if (arr[j] < arrI) {
474 | p[i] = j;
475 | result.push(i);
476 | continue;
477 | }
478 | u = 0;
479 | v = result.length - 1;
480 | while (u < v) {
481 | c = ((u + v) / 2) | 0;
482 | if (arr[result[c]] < arrI) {
483 | u = c + 1;
484 | }
485 | else {
486 | v = c;
487 | }
488 | }
489 | if (arrI < arr[result[u]]) {
490 | if (u > 0) {
491 | p[i] = result[u - 1];
492 | }
493 | result[u] = i;
494 | }
495 | }
496 | u = result.length;
497 | v = result[u - 1];
498 | while (u-- > 0) {
499 | result[u] = v;
500 | v = p[v];
501 | }
502 | return result;
503 | }
504 |
505 | function insertOrAppend(pos, length, newDom, nodes, dom, detectParent) {
506 | const nextPos = pos + 1;
507 | // if (detectParent && newDom.parentNode) {
508 | // return;
509 | // } else
510 | if (nextPos < length) {
511 | dom.insertBefore(newDom, nodes[nextPos].dom);
512 | } else {
513 | dom.appendChild(newDom);
514 | }
515 | }
516 |
517 | function replaceElement(lastVNode, nextVNode, parentDom, mountedQueue, parentVNode, isSVG) {
518 | removeElement(lastVNode, null, nextVNode);
519 | createElement(nextVNode, null, mountedQueue, false, parentVNode, isSVG);
520 | replaceChild(parentDom, lastVNode, nextVNode);
521 | }
522 |
523 | function patchText(lastVNode, nextVNode, parentDom) {
524 | const nextText = nextVNode.children;
525 | const dom = lastVNode.dom;
526 | nextVNode.dom = dom;
527 | if (lastVNode.children !== nextText) {
528 | dom.nodeValue = nextText;
529 | }
530 | }
531 |
532 | export function patchProps(lastVNode, nextVNode, isSVG, isRender) {
533 | const lastProps = lastVNode && lastVNode.props || EMPTY_OBJ;
534 | const nextProps = nextVNode.props;
535 | const dom = nextVNode.dom;
536 | let prop;
537 |
538 | const isInputOrTextArea = (nextVNode.type & (Types.InputElement | Types.TextareaElement)) > 0;
539 | if (nextProps !== EMPTY_OBJ) {
540 | const isFormElement = (nextVNode.type & Types.FormElement) > 0;
541 | for (prop in nextProps) {
542 | patchProp(prop, lastProps[prop], nextProps[prop], dom, isFormElement, isSVG, isInputOrTextArea);
543 | }
544 | if (isFormElement) {
545 | processForm(nextVNode, dom, nextProps, isRender);
546 | }
547 | }
548 | if (lastProps !== EMPTY_OBJ) {
549 | for (prop in lastProps) {
550 | if (
551 | !isSkipProp(prop) &&
552 | isNullOrUndefined(nextProps[prop]) &&
553 | !isNullOrUndefined(lastProps[prop])
554 | ) {
555 | removeProp(prop, lastProps[prop], dom, isInputOrTextArea);
556 | }
557 | }
558 | }
559 | }
560 |
561 | export function patchProp(prop, lastValue, nextValue, dom, isFormElement, isSVG, isInputOrTextArea) {
562 | if (lastValue !== nextValue) {
563 | if (isSkipProp(prop) || isFormElement && prop === 'value') {
564 | return;
565 | } else if (booleanProps[prop]) {
566 | dom[prop] = !!nextValue;
567 | } else if (strictProps[prop]) {
568 | const value = isNullOrUndefined(nextValue) ? '' : nextValue;
569 | // IE8 the value of option is equal to its text as default
570 | // so set it forcely
571 | if (dom[prop] !== value || browser.isIE8) {
572 | dom[prop] = value;
573 | }
574 | // add a private property _value for selecting an non-string value
575 | if (prop === 'value') {
576 | dom._value = value;
577 | }
578 | } else if (isNullOrUndefined(nextValue)) {
579 | removeProp(prop, lastValue, dom, isInputOrTextArea);
580 | } else if (isEventProp(prop)) {
581 | handleEvent(prop.substr(3), lastValue, nextValue, dom);
582 | } else if (isObject(nextValue)) {
583 | patchPropByObject(prop, lastValue, nextValue, dom, isInputOrTextArea);
584 | } else if (prop === 'innerHTML') {
585 | dom.innerHTML = nextValue;
586 | } else {
587 | if (isSVG && namespaces[prop]) {
588 | dom.setAttributeNS(namespaces[prop], prop, nextValue);
589 | } else {
590 | // https://github.com/Javey/Intact/issues/19
591 | // IE 10/11 set placeholder will trigger input event
592 | if (
593 | isInputOrTextArea &&
594 | browser.isIE &&
595 | (browser.version === 10 || browser.version === 11) &&
596 | prop === 'placeholder'
597 | ) {
598 | ignoreInputEvent(dom);
599 | if (nextValue !== '') {
600 | addFocusEvent(dom);
601 | } else {
602 | removeFocusEvent(dom);
603 | }
604 | }
605 | dom.setAttribute(prop, nextValue);
606 | }
607 | }
608 | }
609 | }
610 |
611 | function ignoreInputEvent(dom) {
612 | if (!dom.__ignoreInputEvent) {
613 | const cb = (e) => {
614 | e.stopImmediatePropagation();
615 | delete dom.__ignoreInputEvent;
616 | dom.removeEventListener('input', cb);
617 | };
618 | dom.addEventListener('input', cb);
619 | dom.__ignoreInputEvent = true;
620 | }
621 | }
622 |
623 | function addFocusEvent(dom) {
624 | if (!dom.__addFocusEvent) {
625 | let ignore = false;
626 | const inputCb = (e) => {
627 | if (ignore) e.stopImmediatePropagation();
628 | ignore = false;
629 | };
630 | const focusCb = () => {
631 | ignore = true;
632 | // if we call input.focus(), the input event will not
633 | // be called, so we reset it next tick
634 | setTimeout(() => {
635 | ignore = false;
636 | });
637 | };
638 | dom.addEventListener('input', inputCb);
639 | dom.addEventListener('focusin', focusCb);
640 | dom.addEventListener('focusout', focusCb);
641 | dom.__addFocusEvent = {
642 | focusCb, inputCb
643 | };
644 | }
645 | }
646 |
647 | function removeFocusEvent(dom) {
648 | const cbs = dom.__addFocusEvent;
649 | if (cbs) {
650 | dom.addEventListener('input', cbs.inputCb);
651 | dom.addEventListener('focusin', cbs.focusCb);
652 | dom.addEventListener('focusout', cbs.focusCb);
653 | delete dom.__addFocusEvent;
654 | }
655 | }
656 |
657 | function removeProp(prop, lastValue, dom, isInputOrTextArea) {
658 | if (!isNullOrUndefined(lastValue)) {
659 | switch (prop) {
660 | case 'value':
661 | dom.value = '';
662 | return;
663 | case 'style':
664 | dom.removeAttribute('style');
665 | return;
666 | case 'attributes':
667 | for (let key in lastValue) {
668 | dom.removeAttribute(key);
669 | }
670 | return;
671 | case 'dataset':
672 | removeDataset(lastValue, dom);
673 | return;
674 | case 'innerHTML':
675 | dom.innerHTML = '';
676 | return;
677 | default:
678 | break;
679 | }
680 |
681 | if (booleanProps[prop]) {
682 | dom[prop] = false;
683 | } else if (isEventProp(prop)) {
684 | handleEvent(prop.substr(3), lastValue, null, dom);
685 | } else if (isObject(lastValue)){
686 | const domProp = dom[prop];
687 | try {
688 | dom[prop] = undefined;
689 | delete dom[prop];
690 | } catch (e) {
691 | for (let key in lastValue) {
692 | delete domProp[key];
693 | }
694 | }
695 | } else {
696 | if (
697 | isInputOrTextArea &&
698 | browser.isIE &&
699 | (browser.version === 10 || browser.version === 11) &&
700 | prop === 'placeholder'
701 | ) {
702 | removeFocusEvent(dom);
703 | }
704 | dom.removeAttribute(prop);
705 | }
706 | }
707 | }
708 |
709 | const removeDataset = browser.isIE || browser.isSafari ?
710 | function(lastValue, dom) {
711 | for (let key in lastValue) {
712 | dom.removeAttribute(`data-${kebabCase(key)}`);
713 | }
714 | } :
715 | function(lastValue, dom) {
716 | const domProp = dom.dataset;
717 | for (let key in lastValue) {
718 | delete domProp[key];
719 | }
720 | };
721 |
722 | function patchPropByObject(prop, lastValue, nextValue, dom, isInputOrTextArea) {
723 | if (lastValue && !isObject(lastValue) && !isNullOrUndefined(lastValue)) {
724 | removeProp(prop, lastValue, dom, isInputOrTextArea);
725 | lastValue = null;
726 | }
727 | switch (prop) {
728 | case 'attributes':
729 | return patchAttributes(lastValue, nextValue, dom);
730 | case 'style':
731 | return patchStyle(lastValue, nextValue, dom);
732 | case 'dataset':
733 | return patchDataset(prop, lastValue, nextValue, dom);
734 | default:
735 | return patchObject(prop, lastValue, nextValue, dom);
736 | }
737 | }
738 |
739 | const patchDataset = browser.isIE ?
740 | function patchDataset(prop, lastValue, nextValue, dom) {
741 | let hasRemoved = {};
742 | let key;
743 | let value;
744 |
745 | for (key in nextValue) {
746 | const dataKey = `data-${kebabCase(key)}`;
747 | value = nextValue[key];
748 | if (isNullOrUndefined(value)) {
749 | dom.removeAttribute(dataKey);
750 | hasRemoved[key] = true;
751 | } else {
752 | dom.setAttribute(dataKey, value);
753 | }
754 | }
755 |
756 | if (!isNullOrUndefined(lastValue)) {
757 | for (key in lastValue) {
758 | if (isNullOrUndefined(nextValue[key]) && !hasRemoved[key]) {
759 | dom.removeAttribute(`data-${kebabCase(key)}`);
760 | }
761 | }
762 | }
763 | } : patchObject;
764 |
765 | const _cache = {};
766 | const uppercasePattern = /[A-Z]/g;
767 | export function kebabCase(word) {
768 | if (!_cache[word]) {
769 | _cache[word] = word.replace(uppercasePattern, (item) => {
770 | return `-${item.toLowerCase()}`;
771 | });
772 | }
773 | return _cache[word];
774 | }
775 |
776 | function patchObject(prop, lastValue, nextValue, dom) {
777 | let domProps = dom[prop];
778 | if (isNullOrUndefined(domProps)) {
779 | domProps = dom[prop] = {};
780 | }
781 | let key;
782 | let value;
783 | for (key in nextValue) {
784 | domProps[key] = nextValue[key];
785 | }
786 | if (!isNullOrUndefined(lastValue)) {
787 | for (key in lastValue) {
788 | if (isNullOrUndefined(nextValue[key])) {
789 | delete domProps[key];
790 | }
791 | }
792 | }
793 | }
794 |
795 | function patchAttributes(lastValue, nextValue, dom) {
796 | const hasRemoved = {};
797 | let key;
798 | let value;
799 | for (key in nextValue) {
800 | value = nextValue[key];
801 | if (isNullOrUndefined(value)) {
802 | dom.removeAttribute(key);
803 | hasRemoved[key] = true;
804 | } else {
805 | dom.setAttribute(key, value);
806 | }
807 | }
808 | if (!isNullOrUndefined(lastValue)) {
809 | for (key in lastValue) {
810 | if (isNullOrUndefined(nextValue[key]) && !hasRemoved[key]) {
811 | dom.removeAttribute(key);
812 | }
813 | }
814 | }
815 | }
816 |
817 | function patchStyle(lastValue, nextValue, dom) {
818 | const domStyle = dom.style;
819 | const hasRemoved = {};
820 | let key;
821 | let value;
822 | for (key in nextValue) {
823 | value = nextValue[key];
824 | if (isNullOrUndefined(value)) {
825 | domStyle[key] = '';
826 | hasRemoved[key] = true;
827 | } else {
828 | domStyle[key] = value;
829 | }
830 | }
831 | if (!isNullOrUndefined(lastValue)) {
832 | for (key in lastValue) {
833 | if (isNullOrUndefined(nextValue[key]) && !hasRemoved[key]) {
834 | domStyle[key] = '';
835 | }
836 | }
837 | }
838 | }
839 |
--------------------------------------------------------------------------------
/src/wrappers/input.js:
--------------------------------------------------------------------------------
1 | import {isNullOrUndefined} from '../utils';
2 |
3 | export function processInput(vNode, dom, nextProps) {
4 | const type = nextProps.type;
5 | const value = nextProps.value;
6 | const checked = nextProps.checked;
7 | const defaultValue = nextProps.defaultValue;
8 | const multiple = nextProps.multiple;
9 | const hasValue = !isNullOrUndefined(value);
10 |
11 | if (multiple && multiple !== dom.multiple) {
12 | dom.multiple = multiple;
13 | }
14 | if (!isNullOrUndefined(defaultValue) && !hasValue) {
15 | dom.defaultValue = defaultValue + '';
16 | }
17 | if (isCheckedType(type)) {
18 | if (hasValue) {
19 | dom.value = value;
20 | }
21 | if (!isNullOrUndefined(checked)) {
22 | dom.checked = checked;
23 | }
24 | } else {
25 | if (hasValue && dom.value !== value) {
26 | dom.value = value;
27 | } else if (!isNullOrUndefined(checked)) {
28 | dom.checked = checked;
29 | }
30 | }
31 | }
32 |
33 | function isCheckedType(type) {
34 | return type === 'checkbox' || type === 'radio';
35 | }
36 |
--------------------------------------------------------------------------------
/src/wrappers/process.js:
--------------------------------------------------------------------------------
1 | import {processSelect} from './select';
2 | import {processInput} from './input';
3 | import {processTextarea} from './textarea';
4 | import {Types} from '../vnode';
5 |
6 | export function processForm(vNode, dom, nextProps, isRender) {
7 | const type = vNode.type;
8 | if (type & Types.InputElement) {
9 | processInput(vNode, dom, nextProps, isRender);
10 | } else if (type & Types.TextareaElement) {
11 | processTextarea(vNode, dom, nextProps, isRender);
12 | } else if (type & Types.SelectElement) {
13 | processSelect(vNode, dom, nextProps, isRender);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/wrappers/select.js:
--------------------------------------------------------------------------------
1 | import {isNullOrUndefined, isArray, indexOf} from '../utils';
2 | import {Types} from '../vnode';
3 |
4 | export function processSelect(vNode, dom, nextProps, isRender) {
5 | const multiple = nextProps.multiple;
6 | if (multiple !== dom.multiple) {
7 | dom.multiple = multiple;
8 | }
9 | const children = vNode.children;
10 |
11 | if (!isNullOrUndefined(children)) {
12 | let value = nextProps.value;
13 | if (isRender && isNullOrUndefined(value)) {
14 | value = nextProps.defaultValue;
15 | }
16 |
17 | var flag = {hasSelected: false};
18 | if (isArray(children)) {
19 | for (let i = 0; i < children.length; i++) {
20 | updateChildOptionGroup(children[i], value, flag);
21 | }
22 | } else {
23 | updateChildOptionGroup(children, value, flag);
24 | }
25 | if (!flag.hasSelected) {
26 | dom.value = '';
27 | }
28 | }
29 | }
30 |
31 | function updateChildOptionGroup(vNode, value, flag) {
32 | const tag = vNode.tag;
33 |
34 | if (tag === 'optgroup') {
35 | const children = vNode.children;
36 |
37 | if (isArray(children)) {
38 | for (let i = 0; i < children.length; i++) {
39 | updateChildOption(children[i], value, flag);
40 | }
41 | } else {
42 | updateChildOption(children, value, flag);
43 | }
44 | } else {
45 | updateChildOption(vNode, value, flag);
46 | }
47 | }
48 |
49 | function updateChildOption(vNode, value, flag) {
50 | // skip text and comment node
51 | if (vNode.type & Types.HtmlElement) {
52 | const props = vNode.props;
53 | const dom = vNode.dom;
54 |
55 | if (isArray(value) && indexOf(value, props.value) !== -1 || props.value === value) {
56 | dom.selected = true;
57 | if (!flag.hasSelected) flag.hasSelected = true;
58 | } else if (!isNullOrUndefined(value) || !isNullOrUndefined(props.selected)) {
59 | let selected = !!props.selected;
60 | if (!flag.hasSelected && selected) flag.hasSelected = true;
61 | dom.selected = selected;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/wrappers/textarea.js:
--------------------------------------------------------------------------------
1 | import {isNullOrUndefined} from '../utils';
2 |
3 | export function processTextarea(vNode, dom, nextProps, isRender) {
4 | const value = nextProps.value;
5 | const domValue = dom.value;
6 |
7 | if (isNullOrUndefined(value)) {
8 | if (isRender) {
9 | const defaultValue = nextProps.defaultValue;
10 | if (!isNullOrUndefined(defaultValue)) {
11 | if (defaultValue !== domValue) {
12 | dom.value = defaultValue;
13 | }
14 | } else if (domValue !== '') {
15 | dom.value = '';
16 | }
17 | }
18 | } else {
19 | if (domValue !== value) {
20 | dom.value = value;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test/hydration.js:
--------------------------------------------------------------------------------
1 | import {hydrateRoot} from '../src/hydration';
2 | import {h, hc, renderString, patch, hydrate, render} from '../src';
3 | import assert from 'assert';
4 | import {eqlHtml, isIE8} from './utils';
5 |
6 | function sEql(a, b) {
7 | assert.strictEqual(a, b);
8 | }
9 |
10 | class ClassComponent {
11 | constructor(props) {
12 | this.props = props || {};
13 | }
14 | init() {
15 | this.render();
16 | return this.dom = render(this._vNode);
17 | }
18 | toString() {
19 | this.render();
20 | return renderString(this._vNode);
21 | }
22 | hydrate(vNode, dom) {
23 | this.render();
24 | return this.dom = hydrate(this._vNode, dom, this.mountedQueue, this.parentDom, vNode);
25 | }
26 | update(lastVNode, nextVNode) {
27 | var oldVnode = this._vNode;
28 | this.props = nextVNode.props;
29 | this.render();
30 | return this.dom = patch(oldVnode, this._vNode);
31 | }
32 | render() {
33 | this._vNode = h('span', this.props, this.props.children);
34 | }
35 | }
36 |
37 |
38 | describe('hydrate', () => {
39 | let container;
40 |
41 | beforeEach(() => {
42 | container = document.createElement('div');
43 | document.body.appendChild(container);
44 | });
45 |
46 | afterEach(() => {
47 | document.body.removeChild(container);
48 | });
49 |
50 | function hy(vNode) {
51 | container.innerHTML = renderString(vNode);
52 | hydrateRoot(vNode, container);
53 | }
54 |
55 | it('hydrate element', () => {
56 | const vNode = h('div', {id: 'test'}, 'test', 'test');
57 | container.innerHTML = renderString(vNode);
58 | hydrateRoot(vNode, container);
59 | sEql(vNode.dom, container.firstChild);
60 |
61 | patch(vNode, h('div', null, 'hello'));
62 | eqlHtml(container, 'hello
');
63 | });
64 |
65 | it('hydrate text element', () => {
66 | const vNode = h('div', null, ['test']);
67 | hy(vNode);
68 | sEql(vNode.children[0].dom, container.firstChild.firstChild);
69 |
70 | patch(vNode, h('div', null, ['hello']));
71 | eqlHtml(container, 'hello
');
72 | });
73 |
74 | it('hydrate text elements', () => {
75 | const vNode = h('div', null, ['test1', 'test2']);
76 | hy(vNode);
77 | sEql(vNode.children[0].dom, container.firstChild.childNodes[0]);
78 | sEql(vNode.children[1].dom, container.firstChild.childNodes[1]);
79 | sEql(container.firstChild.childNodes.length, 2);
80 |
81 | patch(vNode, h('div', null, ['test3']));
82 | eqlHtml(container, 'test3
');
83 | });
84 |
85 | it('hydrate comment', () => {
86 | const vNode = h('div', null, hc('test'));
87 | hy(vNode);
88 | sEql(vNode.children.dom, container.firstChild.firstChild);
89 |
90 | patch(vNode, h('div', null, 'test'));
91 | eqlHtml(container, 'test
');
92 | });
93 |
94 | it('hydrate component class', () => {
95 | const vNode = h(ClassComponent, {
96 | className: 'test',
97 | children: [h('i')]
98 | });
99 | hy(vNode);
100 | sEql(vNode.dom, container.firstChild);
101 | sEql(vNode.children._vNode.dom, container.firstChild);
102 | sEql(vNode.children._vNode.children[0].dom, container.firstChild.firstChild);
103 |
104 | patch(vNode, h(ClassComponent, {
105 | className: 'hello',
106 | children: h('b')
107 | }));
108 | eqlHtml(container, '');
109 | });
110 |
111 | it('hydrate svg', () => {
112 | if (isIE8) return;
113 |
114 | const vNode = h('svg', null, h('circle', {cx: 50, cy: 50, r: 50, fill: 'red'}));
115 | hy(vNode);
116 | sEql(vNode.dom, container.firstChild);
117 |
118 | patch(vNode, h('svg', null, h('circle', {cx: 50, cy: 50, r: 50, fill: 'blue'})));
119 | eqlHtml(
120 | container,
121 | [
122 | '',
123 | '',
124 | ]
125 | );
126 | });
127 |
128 | it('should remove redundant elements', () => {
129 | const vNode = h('div');
130 | container.innerHTML = renderString(vNode);
131 | container.appendChild(render(vNode));
132 | container.appendChild(render(vNode));
133 | sEql(container.children.length, 3);
134 | hydrateRoot(vNode, container);
135 | sEql(container.children.length, 1);
136 | });
137 |
138 | it('hydrate empty root', () => {
139 | const vNode = h('div');
140 | hydrateRoot(vNode, container);
141 | sEql(container.firstChild, vNode.dom);
142 | });
143 | });
144 |
--------------------------------------------------------------------------------
/test/patch.js:
--------------------------------------------------------------------------------
1 | import {h, hc, render, patch, remove} from '../src';
2 | import {createTextVNode} from '../src/vnode';
3 | import {removeComponentClassOrInstance} from '../src/vdom';
4 | import assert from 'assert';
5 | import {eqlHtml, isIE8, dispatchEvent} from './utils';
6 | import {browser} from '../src/utils';
7 |
8 | class ClassComponent {
9 | constructor(props) {
10 | this.props = props || {};
11 | }
12 | init() {
13 | this._vNode = h('span', this.props, this.props.children);
14 | return this.dom = render(this._vNode);
15 | }
16 | update(lastVNode, nextVNode) {
17 | var oldVnode = this._vNode;
18 | this._vNode = h('span', nextVNode.props, nextVNode.props.children);
19 | return this.dom = patch(oldVnode, this._vNode);
20 | }
21 | destroy() {
22 | remove(this._vNode);
23 | }
24 | }
25 |
26 | class NewComponent {
27 | constructor(props) {
28 | this.props = props || {};
29 | }
30 | init() {
31 | return this.dom = render(h('section', this.props, this.props.children));
32 | }
33 | }
34 |
35 |
36 | function FunctionComponent(props) {
37 | return h('p', props, props.children);
38 | }
39 |
40 | function NewFunctionComponent(props) {
41 | return h('article', props, props.children);
42 | }
43 |
44 |
45 | describe('Patch', () => {
46 | let container;
47 |
48 | beforeEach(() => {
49 | container = document.createElement('div');
50 | document.body.appendChild(container);
51 | });
52 |
53 | afterEach(() => {
54 | // document.body.removeChild(container);
55 | });
56 |
57 | function reset() {
58 | container.innerHTML = '';
59 | }
60 | function r(vNode) {
61 | reset();
62 | render(vNode, container);
63 | }
64 | function p(lastVNode, nextVNode) {
65 | r(lastVNode);
66 | patch(lastVNode, nextVNode);
67 | }
68 | function eql(lastVNode, nextVNode, html, ie8Html) {
69 | p(lastVNode, nextVNode);
70 | eqlHtml(container, html, ie8Html);
71 | }
72 |
73 | function eqlObj(lastVNode, nextVNode, obj) {
74 | p(lastVNode, nextVNode);
75 | const node = container.firstChild;
76 | if (obj.tag) {
77 | assert.strictEqual(node.tagName.toLowerCase(), obj.tag);
78 | }
79 | if (obj.props) {
80 | for (let i in obj.props) {
81 | assert.strictEqual(node.getAttribute(i), obj.props[i]);
82 | }
83 | }
84 | }
85 |
86 | function sEql(a, b) {
87 | assert.strictEqual(a, b);
88 | }
89 |
90 | it('patch elements', () => {
91 | eql(
92 | h('div'),
93 | h('span'),
94 | ''
95 | );
96 |
97 | eql(
98 | h('div', null, h('span')),
99 | h('div', null, h('div')),
100 | '',
101 | ''
102 | );
103 | });
104 |
105 | it('patch text with vnode', () => {
106 | eql(
107 | h('div', null, 'test'),
108 | h('div', null, h('span')),
109 | '
',
110 | '
'
111 | );
112 | });
113 |
114 | it('patch empty children', () => {
115 | eql(
116 | h('div', null, [undefined]),
117 | h('div', null, [null]),
118 | ''
119 | );
120 |
121 | eql(
122 | h('div', null, []),
123 | h('div', null, []),
124 | ''
125 | );
126 |
127 | eql(
128 | h('div', null, [null]),
129 | h('div', null, []),
130 | ''
131 | );
132 |
133 | eql(
134 | h('div', null, []),
135 | h('div', null, [undefined]),
136 | ''
137 | );
138 | });
139 |
140 | it('patch string child with undefined', () => {
141 | eql(
142 | h('div', null, 'a'),
143 | h('div'),
144 | '',
145 | '
'
146 | );
147 | });
148 |
149 | it('patch empty string child with string', () => {
150 | eql(
151 | h('div', null, ''),
152 | h('div', null, 'a'),
153 | 'a
'
154 | );
155 | });
156 |
157 | it('patch string with array', () => {
158 | eql(
159 | h('div', null, 'a'),
160 | h('div', null, [h('span'), h('span')]),
161 | '
',
162 | '
'
163 | );
164 | });
165 |
166 | it('patch comment', () => {
167 | eql(
168 | hc('div'),
169 | hc('span'),
170 | ''
171 | );
172 | });
173 |
174 | it('patch comment with text', () => {
175 | eql(
176 | h('div', null, ['a', hc('b')]),
177 | h('div', null, [hc('b'), 'a']),
178 | 'a
'
179 | );
180 | });
181 |
182 | it('patch properties', () => {
183 | eql(
184 | h('div', {className: 'a'}),
185 | h('div', {className: 'b'}),
186 | ''
187 | );
188 |
189 | eql(
190 | h('div', {className: 'a'}, h('span', {className: 'aa'})),
191 | h('div', {className: 'b'}, h('span', {className: 'bb'})),
192 | '
'
193 | );
194 |
195 | eql(
196 | h('div', null, [
197 | h('span', {className: 'a'}),
198 | h('div', {className: 'b'})
199 | ]),
200 | h('div', null, [
201 | h('div', {className: 'b'}),
202 | h('span', {className: 'c'})
203 | ]),
204 | '',
205 | ''
206 | );
207 |
208 | eql(
209 | h('div', {className: 'a'}),
210 | h('div'),
211 | ''
212 | );
213 |
214 | eql(
215 | h('div'),
216 | h('div', {className: 'a'}),
217 | ''
218 | );
219 |
220 | eql(
221 | h('div'),
222 | h('div', {className: undefined}),
223 | ''
224 | );
225 | });
226 |
227 | it('patch style', () => {
228 | eql(
229 | h('div', {style: 'color: red; font-size: 20px'}),
230 | h('div', {style: 'color: red;'}),
231 | [
232 | '',
233 | '',
234 | '',
235 | ]
236 | );
237 | eql(
238 | h('div', {style: {color: 'red', fontSize: '20px'}}),
239 | h('div', {style: {color: 'red'}}),
240 | [
241 | '',
242 | '',
243 | '',
244 | ]
245 | );
246 | eql(
247 | h('div', {style: {color: 'red', fontSize: '20px'}}),
248 | h('div', {style: 'color: red;'}),
249 | [
250 | '',
251 | '',
252 | '',
253 | ]
254 | );
255 | eql(
256 | h('div', {style: 'color: red; font-size: 20px'}),
257 | h('div', {style: {color: 'red'}}),
258 | [
259 | '',
260 | '',
261 | '',
262 | ]
263 | );
264 | });
265 |
266 | it('patch dataset', () => {
267 | eqlObj(
268 | h('div', {dataset: {a: 1, b: 'b'}}),
269 | h('div', {dataset: {a: 2, c: 'c'}}),
270 | {tag: 'div', props: {'data-a': '2', 'data-c': 'c', 'data-b': null}}
271 | );
272 | eqlObj(
273 | h('div'),
274 | h('div', {dataset: {a: 2, c: 'c'}}),
275 | {tag: 'div', props: {'data-a': '2', 'data-c': 'c'}}
276 | );
277 | eqlObj(
278 | h('div', {dataset: null}),
279 | h('div', {dataset: {a: 2, c: 'c'}}),
280 | {tag: 'div', props: {'data-a': '2', 'data-c': 'c'}}
281 | );
282 | eqlObj(
283 | h('div'),
284 | h('div', {dataset: {a: 2, c: 'c'}}),
285 | {tag: 'div', props: {'data-a': '2', 'data-c': 'c'}}
286 | );
287 | eql(
288 | h('div', {dataset: {a: 1, b: 'b'}}),
289 | h('div', {dataset: null}),
290 | ''
291 | );
292 | });
293 |
294 | it('patch innerHTML', () => {
295 | eql(
296 | h('div', {innerHTML: 'a'}),
297 | h('div', {innerHTML: 'b'}),
298 | 'b
'
299 | );
300 | eql(
301 | h('div'),
302 | h('div', {innerHTML: 'b'}),
303 | 'b
'
304 | );
305 | eql(
306 | h('div', {innerHTML: 'a'}),
307 | h('div', {innerHTML: undefined}),
308 | [
309 | '',
310 | '
',
311 | ]
312 | );
313 | eql(
314 | h('div', {innerHTML: 'a'}),
315 | h('div'),
316 | [
317 | '',
318 | '
'
319 | ]
320 | );
321 | });
322 |
323 | it('patch attributes', () => {
324 | eqlObj(
325 | h('div', {attributes: {a: 1, b: 'b'}}),
326 | h('div', {attributes: {a: 2, c: 'c'}}),
327 | {div: 'div', props: {a: '2', c: 'c', b: null}}
328 | );
329 |
330 | eql(
331 | h('div', {attributes: {a: 1, b: 'b'}}),
332 | h('div', {attributes: null}),
333 | ''
334 | );
335 |
336 | eql(
337 | h('div', {attributes: {a: 1, b: 'b'}}),
338 | h('div'),
339 | ''
340 | );
341 |
342 | eqlObj(
343 | h('div'),
344 | h('div', {attributes: {a: 2, c: 'c'}}),
345 | {div: 'div', props: {a: '2', c: 'c'}}
346 | );
347 |
348 | eqlObj(
349 | h('div', {attributes: {a: 1, b: 'b'}}),
350 | h('div', {attributes: {a: null, c: 'c'}}),
351 | {div: 'div', props: {a: null, b: null, c: 'c'}}
352 | );
353 | });
354 |
355 | it('patch object property', () => {
356 | eql(
357 | h('div', {p: {a: 1, b: 'b'}}),
358 | h('div', {p: {a: 2, c: 'c'}}),
359 | ''
360 | );
361 | assert.strictEqual(container.firstChild.p.a, 2);
362 | assert.strictEqual(container.firstChild.p.c, 'c');
363 | assert.strictEqual(container.firstChild.p.b, undefined);
364 |
365 | eql(
366 | h('div', {p: {a: 1, b: 'b'}}),
367 | h('div'),
368 | ''
369 | );
370 | assert.strictEqual(container.firstChild.p, undefined);
371 | });
372 |
373 | it('patch input', () => {
374 | p(
375 | h('input', {value: 'a'}),
376 | h('input', {value: null}),
377 | );
378 | assert.strictEqual(container.firstChild.value, '');
379 |
380 | // ie8 does not support change type for input
381 | if (isIE8) return;
382 | eql(
383 | h('input', {type: 'text'}),
384 | h('input', {type: 'password'}),
385 | ''
386 | );
387 | eql(
388 | h('input', {type: 'password'}),
389 | h('input'),
390 | ''
391 | );
392 | });
393 |
394 | it('patch select', () => {
395 | eql(
396 | h('select', null, [
397 | h('option', null, '1'),
398 | h('option', {selected: true}, '2'),
399 | h('option', null, '3'),
400 | ]),
401 | h('select', null, [
402 | h('option', null, '1'),
403 | h('option', null, '2'),
404 | h('option', {selected: true}, '3'),
405 | ]),
406 | '',
407 | ''
408 | );
409 | assert.strictEqual(container.firstChild.children[1].selected, false);
410 | assert.strictEqual(container.firstChild.children[2].selected, true);
411 |
412 | eql(
413 | h('select', null, [
414 | h('option', {key: 1, selected: true}, '1'),
415 | h('option', {key: 2}, '2'),
416 | h('option', {key: 3}, '3'),
417 | ]),
418 | h('select', null, [
419 | h('option', {key: 4, selected: true}, '11'),
420 | h('option', {key: 2}, '22'),
421 | h('option', {key: 3}, '33'),
422 | ]),
423 | '',
424 | ''
425 | );
426 | assert.strictEqual(container.firstChild.children[0].selected, true);
427 | assert.strictEqual(container.firstChild.children[1].selected, false);
428 |
429 | eql(
430 | h('select', null, [
431 | h('option', {key: 2}, '2'),
432 | h('option', {key: 1, selected: true}, '1'),
433 | h('option', {key: 3}, '3'),
434 | ]),
435 | h('select', null, [
436 | h('option', {key: 2}, '22'),
437 | h('option', {key: 4, selected: true}, '11'),
438 | h('option', {key: 3}, '33'),
439 | ]),
440 | '',
441 | ''
442 | );
443 |
444 | assert.strictEqual(container.firstChild.children[0].selected, false);
445 | assert.strictEqual(container.firstChild.children[1].selected, true);
446 | });
447 |
448 | it('patch children', () => {
449 | eql(
450 | h('div', null, [h('span'), 'test', null, undefined, 'hello']),
451 | h('div', null, ['test', h('span', {className: 'a'})]),
452 | 'test
'
453 | );
454 | eql(
455 | h('div', null, [[h('span'), 'test'], [null], [['hello']]]),
456 | h('div', null, [['test'], [h('span', {className: 'a'})]]),
457 | 'test
'
458 | );
459 | });
460 |
461 | it('patch ref', () => {
462 | const a = {};
463 | eql(
464 | h('div', {ref: (dom) => a.dom = dom}),
465 | h('div', {ref: (dom) => a.newDom = dom}),
466 | ''
467 | );
468 | assert.strictEqual(a.dom, null);
469 | assert.strictEqual(a.newDom, container.firstChild);
470 |
471 | eql(
472 | h('div', {ref: (dom) => a.dom = dom}),
473 | h('span', {ref: (dom) => a.newDom = dom}),
474 | ''
475 | );
476 | assert.strictEqual(a.dom, null);
477 | assert.strictEqual(a.newDom, container.firstChild);
478 |
479 | eql(
480 | h('div', {ref: (dom) => a.dom = dom}),
481 | h('div'),
482 | ''
483 | );
484 | assert.strictEqual(a.dom, null);
485 | });
486 |
487 | it('patch class component with element', () => {
488 | eql(
489 | h('div', null, h('div')),
490 | h('div', null, h(ClassComponent)),
491 | '
'
492 | );
493 | });
494 |
495 | it('patch function component with element', () => {
496 | eql(
497 | h('div', null, h('div')),
498 | h('div', null, h(FunctionComponent)),
499 | '',
500 | ''
501 | );
502 | });
503 |
504 | it('patch class component with function component', () => {
505 | eql(
506 | h('div', null, h(ClassComponent)),
507 | h('div', null, h(FunctionComponent)),
508 | '',
509 | ''
510 | );
511 |
512 | eql(
513 | h('div', null, h(FunctionComponent)),
514 | h('div', null, h(ClassComponent)),
515 | '
'
516 | );
517 | });
518 |
519 | it('patch class component with class component', () => {
520 | eql(
521 | h('div', null, [h(ClassComponent), h('i')]),
522 | h('div', null, [h(NewComponent), h('i')]),
523 | '
'
524 | );
525 | });
526 |
527 | it('patch function component with function component', () => {
528 | eql(
529 | h('div', null, h(FunctionComponent)),
530 | h('div', null, h(NewFunctionComponent)),
531 | ''
532 | );
533 | });
534 |
535 | it('patch function component which return an array', () => {
536 | function C(props) {
537 | return [h('div', null, null, props.className), h('span', null, null, props.className)];
538 | }
539 |
540 | eql(
541 | h('div', null, h(C)),
542 | h('div', null, h(C, {className: 'a'})),
543 | '',
544 | ''
545 | );
546 | eql(
547 | h('div', null, h(ClassComponent)),
548 | h('div', null, h(C, {className: 'a'})),
549 | '',
550 | ''
551 | );
552 | });
553 |
554 | it('patch instance component with instance component', () => {
555 | const a = new ClassComponent({children: h('a')});
556 | const b = new ClassComponent({children: h('b')});
557 | const c = new NewComponent();
558 | eql(
559 | h('div', null, a),
560 | h('div', null, b),
561 | '
'
562 | );
563 |
564 | eql(
565 | h('div', null, a),
566 | h('div', null, c),
567 | ''
568 | );
569 | });
570 |
571 | it('patch instance component with class component', () => {
572 | const a = new ClassComponent();
573 | eql(
574 | h('div', null, a),
575 | h('div', null, h(NewComponent)),
576 | ''
577 | );
578 | });
579 |
580 | it('patch instance component with element', () => {
581 | const a = new ClassComponent();
582 | eql(
583 | h('div', null, a),
584 | h('div', null, h('a')),
585 | ''
586 | );
587 | });
588 |
589 | it('patch class component with instance component', () => {
590 | eql(
591 | h('div', null, h(ClassComponent)),
592 | h('div', null, new NewComponent()),
593 | ''
594 | );
595 | });
596 |
597 | it('remove function component', () => {
598 | const o = {};
599 | eql(
600 | h('div', null, h(FunctionComponent, {
601 | children: [
602 | h('b'),
603 | h(ClassComponent, {ref: (i) => o.i = i})
604 | ]
605 | })),
606 | h('div'),
607 | ''
608 | );
609 | sEql(o.i, null);
610 | });
611 |
612 | it('remove class component', () => {
613 | const o = {};
614 | eql(
615 | h('div', null, h(ClassComponent, {
616 | children: [
617 | h('b'),
618 | h(FunctionComponent, {ref: (i) => o.i = i})
619 | ]
620 | })),
621 | h('div'),
622 | ''
623 | );
624 | sEql(o.i, null);
625 | });
626 |
627 | it('update class component', () => {
628 | eql(
629 | h('div', null, h(ClassComponent, {
630 | children: 'a'
631 | })),
632 | h('div', null, h(ClassComponent, {
633 | children: 'b'
634 | })),
635 | 'b
'
636 | );
637 | });
638 |
639 | it('update class component many times', () => {
640 | const vNode1 = h('div', null, h(ClassComponent, {
641 | children: 'a'
642 | }));
643 | const vNode2 = h('div', null, h(ClassComponent, {
644 | children: 'b'
645 | }));
646 | const vNode3 = h('div', null, h(ClassComponent, {
647 | children: 'c'
648 | }));
649 | eql(vNode1, vNode2, 'b
');
650 | patch(vNode2, vNode3);
651 | eqlHtml(container, 'c
');
652 | });
653 |
654 | it('patch single select element', () => {
655 | // safari can not set value to empty
656 | if (browser.isSafari) return;
657 | eql(
658 | h('select', {value: ''}, [
659 | h('option', {value: 1}, '1'),
660 | h('option', {value: 2}, '2')
661 | ]),
662 | h('select', {value: '1'}, [
663 | h('option', {value: 1}, '1'),
664 | h('option', {value: 2}, '2')
665 | ]),
666 | '',
667 | ''
668 | );
669 | assert.strictEqual(container.firstChild.value, '');
670 | assert.strictEqual(container.firstChild.firstChild.selected, false);
671 | assert.strictEqual(container.firstChild.children[1].selected, false);
672 |
673 | eql(
674 | h('select', {value: '1'}, [
675 | h('option', {value: 1}, '1'),
676 | h('option', {value: 2}, '2')
677 | ]),
678 | h('select', {value: ''}, [
679 | h('option', {value: 1}, '1'),
680 | h('option', {value: 2}, '2')
681 | ]),
682 | '',
683 | ''
684 | );
685 | assert.strictEqual(container.firstChild.value, '');
686 | assert.strictEqual(container.firstChild.firstChild.selected, false);
687 | assert.strictEqual(container.firstChild.children[1].selected, false);
688 |
689 | eql(
690 | h('select', {defaultValue: 2}, [
691 | h('option', {value: 1}, '1'),
692 | h('option', {value: 2}, '2')
693 | ]),
694 | h('select', {value: 1}, [
695 | h('option', {value: 1}, '1'),
696 | h('option', {value: 2}, '2')
697 | ]),
698 | [
699 | '',
700 | '',
701 | '',
702 | ]
703 | );
704 | assert.strictEqual(container.firstChild.value, '1');
705 | assert.strictEqual(container.firstChild.firstChild.selected, true);
706 | assert.strictEqual(container.firstChild.children[1].selected, false);
707 | });
708 |
709 | it('patch multiple select element', () => {
710 | p(
711 | h('select', {value: 2, multiple: true}, [
712 | h('option', {value: 1}, '1'),
713 | h('option', {value: 2}, '2')
714 | ]),
715 | h('select', {value: 1, multiple: true}, [
716 | h('option', {value: 1}, '1'),
717 | h('option', {value: 2}, '2')
718 | ])
719 | );
720 | assert.strictEqual(container.firstChild.value, '1');
721 | assert.strictEqual(container.firstChild.firstChild.selected, true);
722 | assert.strictEqual(container.firstChild.children[1].selected, false);
723 |
724 | p(
725 | h('select', {value: 2, multiple: true}, [
726 | h('option', {value: 1}, '1'),
727 | h('option', {value: 2}, '2')
728 | ]),
729 | h('select', {value: '', multiple: true}, [
730 | h('option', {value: 1}, '1'),
731 | h('option', {value: 2}, '2')
732 | ])
733 | );
734 | assert.strictEqual(container.firstChild.value, '');
735 | assert.strictEqual(container.firstChild.firstChild.selected, false);
736 | assert.strictEqual(container.firstChild.children[1].selected, false);
737 |
738 | p(
739 | h('select', {value: '', multiple: true}, [
740 | h('option', {value: 1}, '1'),
741 | h('option', {value: 2}, '2')
742 | ]),
743 | h('select', {value: [1, 2], multiple: true}, [
744 | h('option', {value: 1}, '1'),
745 | h('option', {value: 2}, '2')
746 | ])
747 | );
748 | assert.strictEqual(container.firstChild.firstChild.selected, true);
749 | assert.strictEqual(container.firstChild.children[1].selected, true);
750 |
751 | p(
752 | h('select', {value: [1, 2], multiple: true}, [
753 | h('option', {value: 1}, '1'),
754 | h('option', {value: 2}, '2')
755 | ]),
756 | h('select', {value: [], multiple: true}, [
757 | h('option', {value: 1}, '1'),
758 | h('option', {value: 2}, '2')
759 | ])
760 | );
761 | assert.strictEqual(container.firstChild.firstChild.selected, false);
762 | assert.strictEqual(container.firstChild.children[1].selected, false);
763 |
764 | p(
765 | h('select', {value: [1, 2], multiple: true}, [
766 | h('option', {value: 1}, '1'),
767 | h('option', {value: 2}, '2')
768 | ]),
769 | h('select', {value: '', multiple: true}, [
770 | h('option', {value: 1}, '1'),
771 | h('option', {value: 2}, '2')
772 | ])
773 | );
774 | assert.strictEqual(container.firstChild.firstChild.selected, false);
775 | assert.strictEqual(container.firstChild.children[1].selected, false);
776 | });
777 |
778 | it('patch vNodes which has hoisted', () => {
779 | const vNodes = [
780 | h('div', null, 1),
781 | h('div', null, 2),
782 | h(ClassComponent, {children: '3'}),
783 | 'test',
784 | createTextVNode('text')
785 | ];
786 | eql(
787 | h('div', null, ['a', vNodes, 'b']),
788 | h('div', null, ['a', h('div', null, 0), vNodes, 'b']),
789 | [
790 | '',
791 | 'a\r\n
0
\r\n
1
\r\n
2
3testtextb
',
792 | ]
793 | );
794 | });
795 |
796 | it('patch vNodes which hoisted should clone children', () => {
797 | const vNodes = [
798 | h('span', null, createTextVNode('1')),
799 | h('span', null, createTextVNode('2'))
800 | ];
801 |
802 | const v1 = h('div', null, vNodes);
803 | r(v1);
804 | const v2 = h('div', null, ['a', vNodes]);
805 | patch(v1, v2);
806 | const v3 = h('div', null, vNodes);
807 | patch(v2, v3);
808 | });
809 |
810 | it('patch vNodes which hoisted and with text should clone clildren', () => {
811 | const vNode = h(ClassComponent, {
812 | children: h('span', null, [
813 | '1',
814 | h('span', null, '2'),
815 | '3',
816 | h('span', null, '4')
817 | ])
818 | });
819 |
820 | const v1 = h('div', null, vNode);
821 | r(v1);
822 | v1.children.children.update(null, vNode);
823 | v1.children.children.update(null, vNode);
824 |
825 | eqlHtml(container, '1234
');
826 | });
827 |
828 | it('patch reused vNode', () => {
829 | const child = h('span', null, 'test')
830 | const vNode = h('div', null, child);
831 | const v1 = h('div', null, [
832 | h('div', null, vNode),
833 | h('div', null, vNode),
834 | ]);
835 | r(v1);
836 | const v2 = h('div', null, [
837 | h('div', null, h('div', null, h('span', null, 'changed'))),
838 | h('div', null, vNode),
839 | ]);
840 | p(v1, v2);
841 |
842 | eqlHtml(container, '');
843 | });
844 |
845 | it('patch multiple reused vNodes', () => {
846 | const child = h('span', null, 'test')
847 | const vNode = h('div', null, [child]);
848 | const v1 = h('div', null, [
849 | h('div', null, vNode),
850 | h('div', null, vNode),
851 | ]);
852 | r(v1);
853 | const v2 = h('div', null, [
854 | h('div', null, h('div', null, [h('span', null, 'changed')])),
855 | h('div', null, vNode),
856 | ]);
857 | p(v1, v2);
858 |
859 | eqlHtml(container, '');
860 | });
861 |
862 | describe('Event', () => {
863 | it('patch event', () => {
864 | const fn = sinon.spy();
865 | const newFn = sinon.spy();
866 | p(
867 | h('div', {'ev-click': fn}, 'test'),
868 | h('div', {'ev-click': newFn}, 'test')
869 | );
870 | dispatchEvent(container.firstChild, 'click');
871 | sEql(fn.callCount, 0);
872 | sEql(newFn.callCount, 1);
873 | });
874 |
875 | it('patch event by array', () => {
876 | const fn = sinon.spy();
877 | const newFn1 = sinon.spy();
878 | const newFn2 = sinon.spy();
879 | p(
880 | h('div', {'ev-click': fn}, 'test'),
881 | h('div', {'ev-click': [newFn1, newFn2]}, 'test')
882 | );
883 | dispatchEvent(container.firstChild, 'click');
884 | sEql(fn.callCount, 0);
885 | sEql(newFn1.callCount, 1);
886 | sEql(newFn2.callCount, 1);
887 | });
888 |
889 | it('remove event', () => {
890 | const fn = sinon.spy(() => console.log(111));
891 | p(
892 | h('div', {'ev-click': fn}, 'test'),
893 | h('div', null, 'test')
894 | );
895 | dispatchEvent(container.firstChild, 'click');
896 | sEql(fn.callCount, 0);
897 |
898 | const vNode1 = h('div', null, [h('div', {'ev-click': fn}, 'test')]);
899 | const vNode2 = h('span');
900 | r(vNode1);
901 | const firstDom = container.firstChild;
902 | patch(vNode1, vNode2);
903 | container.appendChild(firstDom);
904 | dispatchEvent(firstDom.firstChild, 'click');
905 | sEql(fn.callCount, 0);
906 | });
907 |
908 | it('remove array event', () => {
909 | const fn = sinon.spy();
910 | p(
911 | h('div', {'ev-click': [fn]}, 'test'),
912 | h('div', null, 'test')
913 | );
914 | dispatchEvent(container.firstChild, 'click');
915 | sEql(fn.callCount, 0);
916 | });
917 |
918 | it('add event', () => {
919 | const fn = sinon.spy();
920 | p(
921 | h('div'),
922 | h('div', {'ev-click': fn})
923 | );
924 | dispatchEvent(container.firstChild, 'click');
925 | sEql(fn.callCount, 1);
926 | });
927 |
928 | it('add event by array', () => {
929 | const fn1 = sinon.spy();
930 | const fn2 = sinon.spy();
931 | p(
932 | h('div'),
933 | h('div', {'ev-click': [fn1, fn2]})
934 | );
935 | dispatchEvent(container.firstChild, 'click');
936 | sEql(fn1.callCount, 1);
937 | sEql(fn2.callCount, 1);
938 | });
939 |
940 | it('patch event on children', () => {
941 | const fn = sinon.spy();
942 | const newFn = sinon.spy();
943 | p(
944 | h('div', null, h('div', {'ev-click': fn})),
945 | h('div', null, h('div', {'ev-click': newFn}))
946 | );
947 | dispatchEvent(container.firstChild.firstChild, 'click');
948 | sEql(fn.callCount, 0);
949 | sEql(newFn.callCount, 1);
950 | });
951 |
952 | it('remove element should remove child node event', () => {
953 | const fn = sinon.spy();
954 | p(
955 | h('div', null, h('div', null, h('div', {'ev-click': fn}))),
956 | h('div')
957 | );
958 | });
959 |
960 | describe('input event in IE10/11', () => {
961 | // if (!(browser.isIE && (browser.version === 10 || browser.version === 11))) return;
962 | it('shuold not trigger input event when set placeholder in IE10/11', (done) => {
963 | const fn = sinon.spy();
964 | p(
965 | h('input', {'ev-input': fn, placeholder: 'a'}),
966 | h('input', {'ev-input': fn, placeholder: 'b'}),
967 | );
968 | sEql(fn.callCount, 0);
969 | container.firstChild.focus();
970 | sEql(fn.callCount, 0);
971 | setTimeout(() => {
972 | container.firstChild.value = 'a';
973 | dispatchEvent(container.firstChild, 'input');
974 | sEql(fn.callCount, 1);
975 | container.firstChild.blur();
976 | sEql(fn.callCount, 1);
977 |
978 | done();
979 | });
980 | });
981 |
982 | it('should remove focus event hack callback when placeholder is empty in IE10/11', (done) => {
983 | const fn = sinon.spy();
984 | p(
985 | h('input', {'ev-input': fn, placeholder: 'a'}),
986 | h('input', {'ev-input': fn, placeholder: ''}),
987 | );
988 | container.firstChild.focus();
989 | sEql(fn.callCount, 0);
990 | setTimeout(() => {
991 | container.firstChild.value = 'a';
992 | dispatchEvent(container.firstChild, 'input');
993 | sEql(fn.callCount, 1);
994 | container.firstChild.blur();
995 | sEql(fn.callCount, 1);
996 |
997 | done();
998 | });
999 | });
1000 |
1001 | it('should remove focus event hack callback when placeholder is removed in IE10/11', (done) => {
1002 | const fn = sinon.spy();
1003 | p(
1004 | h('input', {'ev-input': fn, placeholder: 'a'}),
1005 | h('input', {'ev-input': fn}),
1006 | );
1007 | container.firstChild.focus();
1008 | sEql(fn.callCount, 0);
1009 | setTimeout(() => {
1010 | container.firstChild.value = 'a';
1011 | dispatchEvent(container.firstChild, 'input');
1012 | sEql(fn.callCount, 1);
1013 | container.firstChild.blur();
1014 | sEql(fn.callCount, 1);
1015 |
1016 | done();
1017 | });
1018 | });
1019 | });
1020 | });
1021 |
1022 | describe('Key', () => {
1023 | function map(arr, fn) {
1024 | const ret = [];
1025 | for (let i = 0; i < arr.length; i++) {
1026 | ret.push(fn(arr[i], i));
1027 | }
1028 | return ret;
1029 | }
1030 | function each(arr, fn) {
1031 | for (let i = 0; i< arr.length; i++) {
1032 | fn(arr[i], i);
1033 | }
1034 | }
1035 | function createVNodeFromArray(arr) {
1036 | return h('div', null, map(arr, value => h('span', {key: value}, value)));
1037 | }
1038 | function saveChildren() {
1039 | if (isIE8) {
1040 | const ret = [];
1041 | const children = container.firstChild.children;
1042 | for (let i = 0; i < children.length; i++) {
1043 | ret.push(children[i]);
1044 | }
1045 | return ret;
1046 | }
1047 | return Array.prototype.slice.call(container.firstChild.children, 0);
1048 | }
1049 |
1050 | it('reorder children', () => {
1051 | const vNode = createVNodeFromArray([1, 2, '3', 'test', 'a']);
1052 | r(vNode);
1053 | const childNodes = saveChildren();
1054 |
1055 | patch(vNode, createVNodeFromArray([2, '3', 1, 'a', 'test']));
1056 |
1057 | each([1, 2, 0, 4, 3], (order, index) => {
1058 | sEql(container.firstChild.children[index], childNodes[order]);
1059 | });
1060 | });
1061 |
1062 | it('replace children with non-string keys', () => {
1063 | function createVNodeFromArray(arr) {
1064 | return h('div', null, map(arr, value => h('span', {key: value}, value.key || value)));
1065 | }
1066 | const keys = [1, 2, 3, 4, 5, 6, 7].map(item => ({key: item}));
1067 | const vNode = createVNodeFromArray(keys);
1068 | r(vNode);
1069 | const childNodes = saveChildren();
1070 |
1071 | patch(vNode, createVNodeFromArray(['a', keys[1], keys[2], keys[3], 'b', keys[5], keys[6]]));
1072 | each([null, 1, 2, 3, null, 5, 6], (order, index) => {
1073 | if (order === null) return;
1074 | sEql(container.firstChild.children[index], childNodes[order]);
1075 | });
1076 | });
1077 |
1078 | it('mix keys without keys', () => {
1079 | const vNode = h('div', null, [
1080 | h('span'),
1081 | h('span', {key: 1}),
1082 | h('span', {key: 2}),
1083 | h('span'),
1084 | h('span')
1085 | ]);
1086 | r(vNode);
1087 | const childNodes = saveChildren();
1088 |
1089 | patch(vNode, h('div', null, [
1090 | h('span', {key: 1}),
1091 | h('span'),
1092 | h('span'),
1093 | h('span', {key: 2}),
1094 | h('span')
1095 | ]));
1096 |
1097 | each([1, 0, 3, 2, 4], (order, index) => {
1098 | sEql(container.firstChild.children[index], childNodes[order]);
1099 | });
1100 | });
1101 |
1102 | it('missing key will be removed and insert a new node', () => {
1103 | const vNode = h('div', null, [
1104 | h('span', {key: 1}),
1105 | h('span'),
1106 | h('span')
1107 | ]);
1108 | r(vNode);
1109 | const childNodes = saveChildren();
1110 | patch(vNode, h('div', null, [
1111 | h('span'),
1112 | h('span'),
1113 | h('span')
1114 | ]));
1115 |
1116 | sEql(container.firstChild.children[0], childNodes[1]);
1117 | sEql(container.firstChild.children[1], childNodes[2]);
1118 | sEql(container.firstChild.children[2] === childNodes[0], false);
1119 | });
1120 |
1121 | it('key in component', () => {
1122 | function run(Component) {
1123 | reset();
1124 | function create(arr) {
1125 | return h('div', null, map(arr, value => h(Component, {key: value})));
1126 | }
1127 | const vNode = create([1, 2, 3]);
1128 | r(vNode);
1129 | const childNodes = saveChildren();
1130 | patch(vNode, create([2, 1, 3]));
1131 |
1132 | each([1, 0, 2], (order, index) => {
1133 | sEql(container.firstChild.children[index], childNodes[order]);
1134 | });
1135 | }
1136 |
1137 | run(ClassComponent);
1138 | run(FunctionComponent);
1139 | });
1140 |
1141 | it('key in both component and element', () => {
1142 | const vNode = h('div', null, [
1143 | h('div', {key: 1}),
1144 | h(ClassComponent, {key: 2}),
1145 | h(FunctionComponent, {key: 3})
1146 | ]);
1147 | r(vNode);
1148 | const childNodes = saveChildren();
1149 | patch(vNode, h('div', null, [
1150 | h(FunctionComponent, {key: 3}),
1151 | h('div', {key: 1}),
1152 | h(ClassComponent, {key: 2})
1153 | ]));
1154 |
1155 | each([2, 0, 1], (order, index) => {
1156 | sEql(container.firstChild.children[index], childNodes[order]);
1157 | });
1158 | });
1159 |
1160 | describe('Delete & Insert', () => {
1161 | let children;
1162 | let childNodes;
1163 |
1164 | function create(lastKeys, nextKeys) {
1165 | const vNode = createVNodeFromArray(lastKeys);
1166 | r(vNode);
1167 | childNodes = saveChildren();
1168 | patch(vNode, createVNodeFromArray(nextKeys));
1169 | children = container.firstChild.children;
1170 | }
1171 |
1172 | it('delete key at the start', () => {
1173 | create([1, 2, 3], [2, 3]);
1174 | sEql(children.length, 2);
1175 | sEql(children[0], childNodes[1]);
1176 | sEql(children[1], childNodes[2]);
1177 | });
1178 |
1179 | it('delete key at the center', () => {
1180 | create([1, 2, 3], [1, 3]);
1181 | sEql(children.length, 2);
1182 | sEql(children[0], childNodes[0]);
1183 | sEql(children[1], childNodes[2]);
1184 | });
1185 |
1186 | it('delete key at the end', () => {
1187 | create([1, 2, 3], [1, 2]);
1188 | sEql(children.length, 2);
1189 | sEql(children[0], childNodes[0]);
1190 | sEql(children[1], childNodes[1]);
1191 | });
1192 |
1193 | it('insert key to the start', () => {
1194 | create([2, 3], [1, 2, 3]);
1195 | sEql(children.length, 3);
1196 | sEql(children[1], childNodes[0]);
1197 | sEql(children[2], childNodes[1]);
1198 | });
1199 |
1200 | it('insert key to the center', () => {
1201 | create([1, 3], [1, 2, 3]);
1202 | sEql(children.length, 3);
1203 | sEql(children[0], childNodes[0]);
1204 | sEql(children[2], childNodes[1]);
1205 | });
1206 |
1207 | it('insert key to the end', () => {
1208 | create([1, 2], [1, 2, 3]);
1209 | sEql(children.length, 3);
1210 | sEql(children[0], childNodes[0]);
1211 | sEql(children[1], childNodes[1]);
1212 | });
1213 |
1214 | it('insert to start and delete from center', () => {
1215 | create([2, 3, 4], [1, 2, 4]);
1216 | sEql(children.length, 3);
1217 | sEql(children[1], childNodes[0]);
1218 | sEql(children[2], childNodes[2]);
1219 | });
1220 |
1221 | it('insert to end and delete from center', () => {
1222 | create([1, 2, 3], [1, 3, 4]);
1223 | sEql(children.length, 3);
1224 | sEql(children[0], childNodes[0]);
1225 | sEql(children[1], childNodes[2]);
1226 | });
1227 |
1228 | it('insert multiple keys and delete multiple keys', () => {
1229 | create([1, 2, 3, 4, 5, 6, 7, 8], [11, 3, 5, 4, 9, 10, 1]);
1230 | sEql(children.length, 7);
1231 | each([[1, 2], [2, 4], [3, 3], [6, 0]], ([order, index]) => {
1232 | sEql(children[order], childNodes[index]);
1233 | });
1234 | });
1235 |
1236 | it('replace all keys', () => {
1237 | create([1, 2, 3], [4, 5, 6, 7]);
1238 | sEql(children.length, 4);
1239 | for (let i = 0; i < 4; i++) {
1240 | sEql(children[i] === childNodes[i], false);
1241 | }
1242 | });
1243 | });
1244 | });
1245 |
1246 | describe('Component', () => {
1247 | let Component;
1248 | let NewComponent;
1249 | let _p;
1250 | let _np;
1251 |
1252 | function createComponent() {
1253 | function Component(props) {
1254 | this.props = props || {};
1255 | }
1256 | Component.prototype.init = sinon.spy(function(lastVNode, nextVNode) {
1257 | if (lastVNode) removeComponentClassOrInstance(lastVNode, null, nextVNode);
1258 | this.vNode = h('span', this.props, this.props.children);
1259 | return this.dom = render(this.vNode);
1260 | });
1261 | Component.prototype.mount = sinon.spy();
1262 | Component.prototype.update = sinon.spy(function() {
1263 | return render(h('div', this.props, this.props.children));
1264 | });
1265 | Component.prototype.destroy = sinon.spy(function() {
1266 | remove(this.vNode);
1267 | });
1268 |
1269 | return Component;
1270 | }
1271 |
1272 | beforeEach(() => {
1273 | Component = createComponent();
1274 | _p = Component.prototype;
1275 | NewComponent = createComponent();
1276 | _np = NewComponent.prototype;
1277 | });
1278 |
1279 | it('call init and mount method once and don\'t call update and destroy method when render', () => {
1280 | r(h(Component));
1281 |
1282 | sEql(_p.init.callCount, 1);
1283 | sEql(_p.mount.callCount, 1);
1284 | sEql(_p.update.callCount, 0);
1285 | sEql(_p.destroy.callCount, 0);
1286 | sEql(_p.mount.calledAfter(_p.init), true);
1287 | });
1288 |
1289 | it('only call update method once when update', () => {
1290 | eql(h(Component), h(Component), '');
1291 |
1292 | sEql(_p.init.callCount, 1);
1293 | sEql(_p.mount.callCount, 1);
1294 | sEql(_p.update.callCount, 1);
1295 | sEql(_p.destroy.callCount, 0);
1296 | sEql(_p.update.calledAfter(_p.mount), true);
1297 | });
1298 |
1299 | it('only call destroy method once when destroy', () => {
1300 | p(h(Component), h(NewComponent));
1301 |
1302 | sEql(_p.init.callCount, 1);
1303 | sEql(_p.mount.callCount, 1);
1304 | sEql(_p.update.callCount, 0);
1305 | sEql(_p.destroy.callCount, 1);
1306 | sEql(_p.destroy.calledAfter(_p.mount), true);
1307 |
1308 | sEql(_np.init.callCount, 1);
1309 | sEql(_np.mount.callCount, 1);
1310 | sEql(_np.update.callCount, 0);
1311 | sEql(_np.destroy.callCount, 0);
1312 | });
1313 |
1314 | it('this should pointer to the instance of component', () => {
1315 | p(h(Component), h(NewComponent));
1316 |
1317 | sEql(_p.init.thisValues[0] instanceof Component, true);
1318 | sEql(_p.mount.thisValues[0] instanceof Component, true);
1319 | sEql(_p.destroy.thisValues[0] instanceof Component, true);
1320 | sEql(_np.init.thisValues[0] instanceof NewComponent, true);
1321 | sEql(_np.mount.thisValues[0] instanceof NewComponent, true);
1322 | });
1323 |
1324 | it('don\'t replace when return the same dom between different components', () => {
1325 | _np.init = function(lastVNode, vNode) {
1326 | return this.dom = lastVNode.dom;
1327 | };
1328 |
1329 | const vNode = h(Component);
1330 | r(vNode);
1331 | const dom = container.firstChild;
1332 | patch(vNode, h(NewComponent));
1333 | sEql(dom, container.firstChild);
1334 | });
1335 |
1336 | it('check the args for method when update', () => {
1337 | const lastVNode = h(Component);
1338 | const nextVNode = h(Component);
1339 | p(lastVNode, nextVNode);
1340 |
1341 | sEql(_p.init.calledWithExactly(null, lastVNode), true);
1342 | sEql(_p.mount.calledWithExactly(null, lastVNode), true);
1343 | sEql(_p.update.calledWithExactly(lastVNode, nextVNode), true);
1344 | });
1345 |
1346 | it('check the args for method when destroy', () => {
1347 | const lastVNode = h(Component);
1348 | const nextVNode = h(NewComponent);
1349 | p(lastVNode, nextVNode);
1350 |
1351 | sEql(_p.init.calledWithExactly(null, lastVNode), true);
1352 | sEql(_p.mount.calledWithExactly(null, lastVNode), true);
1353 | sEql(_p.destroy.calledWithExactly(lastVNode, nextVNode, null), true);
1354 | });
1355 |
1356 | it('should destroy children when destroy class component', () => {
1357 | const C = createComponent();
1358 | const cp = C.prototype;
1359 |
1360 | eql(
1361 | h('div', null, h(Component, {children: h(C)})),
1362 | h('div', null, h(NewComponent)),
1363 | '
'
1364 | );
1365 |
1366 | sEql(cp.init.callCount, 1);
1367 | sEql(cp.mount.callCount, 1);
1368 | sEql(cp.update.callCount, 0);
1369 | sEql(cp.destroy.callCount, 1);
1370 | });
1371 |
1372 | it('check method for instance component replacing', () => {
1373 | eql(
1374 | h('div', null, new Component()),
1375 | h('div', null, new NewComponent()),
1376 | '
'
1377 | );
1378 |
1379 | sEql(_p.init.callCount, 1);
1380 | sEql(_p.mount.callCount, 1);
1381 | sEql(_p.update.callCount, 0);
1382 | sEql(_p.destroy.callCount, 1);
1383 | sEql(_np.init.callCount, 1);
1384 | sEql(_np.mount.callCount, 1);
1385 | sEql(_np.update.callCount, 0);
1386 | sEql(_np.destroy.callCount, 0);
1387 | });
1388 |
1389 | it('check method for instance component updating', () => {
1390 | const c = new Component();
1391 | eql(
1392 | h('div', null, c),
1393 | h('div', null, c),
1394 | '',
1395 | ''
1396 | );
1397 |
1398 | sEql(_p.init.callCount, 1);
1399 | sEql(_p.mount.callCount, 1);
1400 | sEql(_p.update.callCount, 1);
1401 | sEql(_p.destroy.callCount, 0);
1402 | });
1403 |
1404 | it('should destroy children when destroy instance component', () => {
1405 | const C = createComponent();
1406 | const cp = C.prototype;
1407 |
1408 | eql(
1409 | h('div', null, new Component({children: h(C)})),
1410 | h('div', null, new NewComponent()),
1411 | '
'
1412 | );
1413 |
1414 | sEql(cp.init.callCount, 1);
1415 | sEql(cp.mount.callCount, 1);
1416 | sEql(cp.update.callCount, 0);
1417 | sEql(cp.destroy.callCount, 1);
1418 | });
1419 | });
1420 |
1421 | describe('SVG', () => {
1422 | if (isIE8) return;
1423 |
1424 | it('patch svg', () => {
1425 | p(
1426 | h('svg', null, h('circle', {cx: 50, cy: 50, r: 50, fill: 'red'})),
1427 | h('svg', null, h('circle', {cx: 50, cy: 50, r: 50, fill: 'blue'})),
1428 | );
1429 | sEql(container.firstChild.firstChild.getAttribute('fill'), 'blue');
1430 | });
1431 | });
1432 | });
1433 |
--------------------------------------------------------------------------------
/test/render.js:
--------------------------------------------------------------------------------
1 | import {h, hc, render} from '../src';
2 | import assert from 'assert';
3 | import {innerHTML, eqlHtml, dispatchEvent, isIE8} from './utils';
4 | import {MountedQueue, svgNS, browser, indexOf} from '../src/utils';
5 |
6 | class ClassComponent {
7 | constructor(props) {
8 | this.props = props || {};
9 | }
10 | init() {
11 | return render(h('span', this.props, this.props.children));
12 | }
13 | }
14 |
15 | function FunctionComponent(props) {
16 | return h('p', {
17 | className: props.className
18 | }, props.children);
19 | }
20 |
21 | describe('Render', () => {
22 | let container;
23 |
24 | beforeEach(() => {
25 | container = document.createElement('div');
26 | document.body.appendChild(container);
27 | });
28 |
29 | afterEach(() => {
30 | // document.body.removeChild(container);
31 | });
32 |
33 | function reset() {
34 | container.innerHTML = '';
35 | }
36 | function r(vNode) {
37 | reset();
38 | render(vNode, container);
39 | }
40 | function eql(vNode, html, ie8Html) {
41 | r(vNode);
42 | eqlHtml(container, html, ie8Html);
43 | }
44 | function eqlObj(vNode, obj) {
45 | r(vNode);
46 | const node = container.firstChild;
47 | if (obj.tag) {
48 | assert.strictEqual(node.tagName.toLowerCase(), obj.tag);
49 | }
50 | if (obj.props) {
51 | for (let i in obj.props) {
52 | assert.strictEqual(node.getAttribute(i), obj.props[i]);
53 | }
54 | }
55 | }
56 |
57 | it('render null', () => {
58 | eql(null, '');
59 | });
60 |
61 | it('render div', () => {
62 | eql(h('div'), '');
63 | assert.strictEqual(container.children.length, 1);
64 | });
65 |
66 | it('render comment', () => {
67 | eql(hc('comment'), '');
68 | });
69 |
70 | it('render invalid node should throw an error', () => {
71 | assert.throws(function() {eql(h('div', null, true));});
72 | });
73 |
74 | it('render properties', () => {
75 | const div = h('div', {test: 'test', className: 'test'});
76 | eqlObj(div, {tag: 'div', props: {'class': 'test', test: 'test'}});
77 | assert.strictEqual(container.children.length, 1);
78 | });
79 |
80 | it('render style', () => {
81 | // the ';' at last is missing in ie
82 | const style = 'color: red; font-size: 20px';
83 | r(h('div', {style: 'color: red; font-size: 20px'}));
84 | assert.strictEqual(
85 | indexOf(
86 | [style, 'font-size: 20px; color: red'],
87 | container.firstChild.getAttribute('style')
88 | .toLowerCase().substr(0, style.length)
89 | ) > -1,
90 | true
91 | );
92 |
93 | r(h('div', {style: {color: 'red', fontSize: '20px'}}));
94 | assert.strictEqual(
95 | indexOf(
96 | [style, 'font-size: 20px; color: red'],
97 | container.firstChild.getAttribute('style')
98 | .toLowerCase().substr(0, style.length)
99 | ) > -1,
100 | true
101 | );
102 | });
103 |
104 | it('render dataset', () => {
105 | eqlObj(
106 | h('div', {dataset: {a: 1, b: 'b', aA: 'a'}}),
107 | {
108 | tag: 'div',
109 | props: {
110 | 'data-a': '1',
111 | 'data-b': 'b',
112 | 'data-a-a': 'a'
113 | }
114 | }
115 | );
116 | });
117 |
118 | it('render attributes', () => {
119 | eqlObj(
120 | h('div', {attributes: {a: 1, b: 'b'}}),
121 | {tag: 'div', props: {a: '1', b: 'b'}}
122 | );
123 | });
124 |
125 | it('render object property', () => {
126 | eql(
127 | h('div', {a: {b: 1}}),
128 | ''
129 | );
130 | assert.strictEqual(container.firstChild.a.b, 1);
131 | });
132 |
133 | it('render children', () => {
134 | eql(
135 | h('div', {className: 'test'}, 'test'),
136 | 'test
'
137 | );
138 | eql(
139 | h('div', null, ['text', 0]),
140 | 'text0
'
141 | );
142 | eql(
143 | h('div', null, ['text', h('div')]),
144 | '',
145 | ''
146 | );
147 | eql(
148 | h('div', {}, [undefined, 'text']),
149 | 'text
'
150 | );
151 | });
152 |
153 | it('render empty array children', () => {
154 | eql(
155 | h('div', null, []),
156 | ''
157 | );
158 | });
159 |
160 | it('render nested children', () => {
161 | eql(
162 | h('div', null, [['text', [h('div')]]]),
163 | '',
164 | ''
165 | );
166 | });
167 |
168 | it('render function component children', () => {
169 | function Component(props) {
170 | return h('span', {
171 | className: props.className
172 | }, props.children);
173 | }
174 | eql(
175 | h('div', null, h(Component, {
176 | className: 'component',
177 | children: 'text'
178 | })),
179 | 'text
'
180 | );
181 | eql(
182 | h('div', null, h(Component, {
183 | className: 'component'
184 | })),
185 | '
'
186 | );
187 | eql(
188 | h('div', null, h(Component, {
189 | className: 'component',
190 | children: h(Component)
191 | })),
192 | '
'
193 | );
194 | });
195 |
196 | it('render class component children', () => {
197 | class Component {
198 | constructor(props) {
199 | this.props = props;
200 | }
201 | init() {
202 | return render(h('span', this.props, this.props.children));
203 | }
204 | }
205 | eql(
206 | h('div', null, h(Component, {
207 | className: 'test'
208 | })),
209 | '
'
210 | );
211 | eql(
212 | h('div', null, h(Component, {
213 | className: 'test',
214 | children: 'text'
215 | })),
216 | 'text
'
217 | );
218 | eql(
219 | h('div', null, h(Component, {
220 | className: 'test',
221 | children: h(Component)
222 | })),
223 | '
'
224 | );
225 | eql(
226 | h('div', null, h(Component, {
227 | className: 'test'
228 | }, h(Component))),
229 | '
'
230 | );
231 | eql(
232 | h('div', null, h(Component, {
233 | className: 'test',
234 | children: 'ignore'
235 | }, h(Component))),
236 | '
'
237 | );
238 | eql(
239 | h('div', null, h(Component, null, 'a')),
240 | 'a
'
241 | );
242 | eql(
243 | h('div', null, h(Component)),
244 | '
'
245 | );
246 | });
247 |
248 | it('render class component in function component', () => {
249 | eql(
250 | h('div', null, h(FunctionComponent, {
251 | children: [
252 | h(ClassComponent),
253 | h('i')
254 | ]
255 | })),
256 | '',
257 | ''
258 | );
259 | });
260 |
261 | it('render function component in class component', () => {
262 | eql(
263 | h('div', null, h(ClassComponent, {
264 | children: [
265 | h(FunctionComponent),
266 | h('i')
267 | ]
268 | })),
269 | '',
270 | ''
271 | );
272 | });
273 |
274 | it('render function component which return an array', () => {
275 | function C(props) {
276 | return [h('div', null, null, props.className), h('span', null, null, props.className)];
277 | }
278 | eql(
279 | h('div', null, h(C, {className: 'a'})),
280 | '',
281 | ''
282 | );
283 | });
284 |
285 | it('render div with ref', () => {
286 | const o = {};
287 | eql(
288 | h('div', {ref: (dom) => o.dom = dom, className: 'test'}),
289 | ''
290 | );
291 | assert.strictEqual(o.dom, container.firstChild);
292 | });
293 |
294 | it('render function component with ref', () => {
295 | const o = {};
296 | function C(props) {
297 | return h('span', props, props.children);
298 | }
299 | eql(
300 | h(C, {
301 | ref: (dom) => o.dom = dom,
302 | className: 'test',
303 | children: 'text'
304 | }),
305 | 'text'
306 | );
307 | assert.strictEqual(o.dom, container.firstChild);
308 | });
309 |
310 | it('render class component with ref', () => {
311 | const o = {};
312 | class C {
313 | constructor(props) {
314 | this.props = props;
315 | }
316 | init() {
317 | o._instance = this;
318 | return render(h('span', this.props, this.props.children));
319 | }
320 | }
321 | eql(
322 | h(C, {
323 | ref: (instance) => o.instance = instance,
324 | className: 'test',
325 | children: 'text'
326 | }),
327 | 'text'
328 | );
329 | assert.strictEqual(o.instance, o._instance);
330 | });
331 |
332 | it('render ref with nested component', () => {
333 | const o = {};
334 | eql(
335 | h(ClassComponent, {
336 | children: [
337 | h('span', {ref: (i) => o.i = i})
338 | ]
339 | }),
340 | ''
341 | );
342 | assert.strictEqual(o.i, container.firstChild.firstChild);
343 |
344 | eql(
345 | h(FunctionComponent, {
346 | children: [
347 | h(ClassComponent, {ref: (i) => o.j = i})
348 | ]
349 | }),
350 | '
'
351 | );
352 | assert.strictEqual(o.j instanceof ClassComponent, true);
353 | });
354 |
355 | it('render component instance', () => {
356 | let i = new ClassComponent();
357 | eql(
358 | h('div', null, i),
359 | '
'
360 | );
361 | eql(
362 | h(i),
363 | ''
364 | );
365 |
366 | i = new ClassComponent({className: 'a'});
367 | eql(
368 | h('div', null, [i]),
369 | '
'
370 | );
371 |
372 | const o = {};
373 | i = new ClassComponent({className: 'a', ref: (i) => o.i = i});
374 | eql(
375 | h('div', null, i),
376 | '
'
377 | );
378 | assert.strictEqual(o.i === i, true);
379 | });
380 |
381 | it('render input', () => {
382 | r(h('input', {value: 0}));
383 | assert.strictEqual(container.firstChild.value, '0');
384 |
385 | r(h('input', {value: true}));
386 | assert.strictEqual(container.firstChild.value, 'true');
387 |
388 | r(h('input', {value: false}));
389 | assert.strictEqual(container.firstChild.value, 'false');
390 |
391 | r(h('input', {value: ''}));
392 | assert.strictEqual(container.firstChild.value, '');
393 |
394 | r(h('input', {value: '1'}));
395 | assert.strictEqual(container.firstChild.value, '1');
396 |
397 | r(h('input', {value: undefined}));
398 | assert.strictEqual(container.firstChild.value, '');
399 |
400 | r(h('input', {value: null}));
401 | assert.strictEqual(container.firstChild.value, '');
402 | });
403 |
404 | it('render single select element', () => {
405 | if (browser.isSafari) return;
406 | eql(
407 | h('select', {value: ''}, [
408 | h('option', {value: 1}, '1'),
409 | h('option', {value: 2}, '2')
410 | ]),
411 | '',
412 | ''
413 | );
414 | assert.strictEqual(container.firstChild.value, '');
415 | assert.strictEqual(container.firstChild.firstChild.selected, false);
416 | assert.strictEqual(container.firstChild.children[1].selected, false);
417 |
418 | eql(
419 | h('select', {value: 2}, [
420 | h('option', {value: 1}, '1'),
421 | h('option', {value: 2}, '2')
422 | ]),
423 | [
424 | '',
425 | '',
426 | '',
427 | ]
428 | );
429 | assert.strictEqual(container.firstChild.value, '2');
430 | assert.strictEqual(container.firstChild.firstChild.selected, false);
431 | assert.strictEqual(container.firstChild.children[1].selected, true);
432 |
433 | eql(
434 | h('select', {defaultValue: 2}, [
435 | h('option', {value: 1}, '1'),
436 | h('option', {value: 2}, '2')
437 | ]),
438 | [
439 | '',
440 | '',
441 | ''
442 | ]
443 | );
444 | assert.strictEqual(container.firstChild.value, '2');
445 | assert.strictEqual(container.firstChild.firstChild.selected, false);
446 | assert.strictEqual(container.firstChild.children[1].selected, true);
447 |
448 | r(
449 | h('select', {value: '1'}, [
450 | h('option', {value: 1}, '1'),
451 | h('option', {value: 2}, '2')
452 | ])
453 | );
454 | assert.strictEqual(container.firstChild.value, '');
455 | assert.strictEqual(container.firstChild.firstChild.selected, false);
456 | assert.strictEqual(container.firstChild.children[1].selected, false);
457 | });
458 |
459 | it('render multiple select element', () => {
460 | r(
461 | h('select', {value: 2, multiple: true}, [
462 | h('option', {value: 1}, '1'),
463 | h('option', {value: 2}, '2')
464 | ])
465 | );
466 | // FIXME: it can not select the second value in android 4.4
467 | assert.strictEqual(container.firstChild.value, '2');
468 | assert.strictEqual(container.firstChild.firstChild.selected, false);
469 | assert.strictEqual(container.firstChild.children[1].selected, true);
470 |
471 | r(
472 | h('select', {value: '', multiple: true}, [
473 | h('option', {value: 1}, '1'),
474 | h('option', {value: 2}, '2')
475 | ])
476 | );
477 | assert.strictEqual(container.firstChild.value, '');
478 | assert.strictEqual(container.firstChild.firstChild.selected, false);
479 | assert.strictEqual(container.firstChild.children[1].selected, false);
480 |
481 | r(
482 | h('select', {value: [2], multiple: true}, [
483 | h('option', {value: 1}, '1'),
484 | h('option', {value: 2}, '2')
485 | ])
486 | );
487 | assert.strictEqual(container.firstChild.firstChild.selected, false);
488 | assert.strictEqual(container.firstChild.children[1].selected, true);
489 |
490 | r(
491 | h('select', {value: [1, 2], multiple: true}, [
492 | h('option', {value: 1}, '1'),
493 | h('option', {value: 2}, '2')
494 | ])
495 | );
496 | assert.strictEqual(container.firstChild.firstChild.selected, true);
497 | assert.strictEqual(container.firstChild.children[1].selected, true);
498 |
499 | r(
500 | h('select', {value: [1, '2'], multiple: true}, [
501 | h('option', {value: 1}, '1'),
502 | h('option', {value: 2}, '2')
503 | ])
504 | );
505 | assert.strictEqual(container.firstChild.firstChild.selected, true);
506 | assert.strictEqual(container.firstChild.children[1].selected, false);
507 | });
508 |
509 | describe('Event', () => {
510 | it('attach event listener', () => {
511 | const fn = sinon.spy();
512 | r(h('div', {'ev-click': fn}));
513 | dispatchEvent(container.firstChild, 'click');
514 | assert.strictEqual(fn.callCount, 1);
515 | assert.strictEqual(fn.args[0].length, 1);
516 | assert.strictEqual(fn.args[0][0].type, 'click');
517 | assert.strictEqual(fn.args[0][0].target, container.firstChild);
518 | assert.strictEqual(fn.args[0][0].currentTarget, container.firstChild);
519 |
520 | dispatchEvent(container.firstChild, 'click');
521 | assert.strictEqual(fn.callCount, 2);
522 | });
523 |
524 | it('attach event listener by array', () => {
525 | const fn1 = sinon.spy();
526 | const fn2 = sinon.spy();
527 | r(h('div', {'ev-click': [fn1, fn2]}));
528 | dispatchEvent(container.firstChild, 'click');
529 | assert.strictEqual(fn1.callCount, 1);
530 | assert.strictEqual(fn2.callCount, 1);
531 |
532 | dispatchEvent(container.firstChild, 'click');
533 | assert.strictEqual(fn1.callCount, 2);
534 | assert.strictEqual(fn2.callCount, 2);
535 | });
536 |
537 | it('trigger event on child node', () => {
538 | const fn = sinon.spy();
539 | r(h('div', {'ev-click': fn}, h('div')));
540 | dispatchEvent(container.firstChild.firstChild, 'click');
541 | assert.strictEqual(fn.callCount, 1);
542 | assert.strictEqual(fn.args[0][0].target, container.firstChild.firstChild);
543 | assert.strictEqual(fn.args[0][0].currentTarget, container.firstChild);
544 | });
545 |
546 | it('event bubble', () => {
547 | const currentTargets = [];
548 | const fn1 = sinon.spy((e) => currentTargets.push(e.currentTarget));
549 | const fn2 = sinon.spy((e) => currentTargets.push(e.currentTarget));
550 | r(h('p', {'ev-click': fn2}, h('span', {'ev-click': fn1})));
551 | dispatchEvent(container.firstChild.firstChild, 'click');
552 | assert.strictEqual(fn1.callCount, 1);
553 | assert.strictEqual(fn2.callCount, 1);
554 | assert.strictEqual(fn2.calledAfter(fn1), true);
555 | assert.strictEqual(fn1.args[0][0].target, container.firstChild.firstChild);
556 | assert.strictEqual(currentTargets[0], container.firstChild.firstChild);
557 | assert.strictEqual(fn2.args[0][0].target, container.firstChild.firstChild);
558 | assert.strictEqual(currentTargets[1], container.firstChild);
559 | });
560 |
561 | it('stop event bubble', () => {
562 | const fn1 = sinon.spy((e) => e.stopPropagation());
563 | const fn2 = sinon.spy();
564 | r(h('p', {'ev-click': fn2}, h('span', {'ev-click': fn1}, 'span')));
565 | dispatchEvent(container.firstChild.firstChild, 'click');
566 | assert.strictEqual(fn1.callCount, 1);
567 | assert.strictEqual(fn2.callCount, 0);
568 | });
569 |
570 | it('prevent default', () => {
571 | const url = location.href;
572 | const fn = sinon.spy((e) => e.preventDefault());
573 | r(h('a', {'ev-click': fn, href: "https://www.baidu.com"}, 'test'));
574 | dispatchEvent(container.firstChild, 'click');
575 | assert.strictEqual(location.href, url);
576 | });
577 |
578 | it('mouseenter & mouseleave event', () => {
579 | const fn1 = sinon.spy(() => {});
580 | const fn2 = sinon.spy(() => {});
581 | r(h('div', {
582 | 'ev-mouseenter': fn1,
583 | 'ev-mouseleave': fn2
584 | }, 'test'));
585 | dispatchEvent(container.firstChild, 'mouseenter');
586 | dispatchEvent(container.firstChild, 'mouseleave');
587 | assert.strictEqual(fn1.callCount, 1);
588 | assert.strictEqual(fn2.callCount, 1);
589 | });
590 | });
591 |
592 | describe('Class Component', () => {
593 | let C;
594 | let init;
595 | let mount;
596 | let P;
597 | let pInit;
598 | let pMount;
599 |
600 | beforeEach(() => {
601 | init = sinon.spy(function(lastVNode, vNode) {
602 | return render(h('div', vNode.props, vNode.props.children), null, this.mountedQueue);
603 | });
604 | mount = sinon.spy((lastVNode, vNode) => {
605 | assert.strictEqual(container.firstChild, vNode.dom);
606 | });
607 | function CC(props) {
608 | this.props = props;
609 | }
610 | CC.prototype.init = init;
611 | CC.prototype.mount = mount;
612 | C = CC;
613 |
614 | pInit = sinon.spy(function(lastVNode, nextVNode) {
615 | return render(h('div', null, 'test'), null, this.mountedQueue);
616 | });
617 | pMount = sinon.spy((lastVNode, nextVNode) => {
618 | assert.strictEqual(container.firstChild.firstChild, nextVNode.dom);
619 | });
620 | function PP(props) {
621 | this.props = props;
622 | }
623 | PP.prototype.init = pInit;
624 | PP.prototype.mount = pMount;
625 | P = PP;
626 | });
627 |
628 | it('init and mount', () => {
629 | const vNode = h(C, {className: 'test', children: 'text'});
630 | r(vNode);
631 | assert.strictEqual(init.callCount, 1);
632 | assert.strictEqual(init.calledWith(null, vNode), true);
633 | assert.strictEqual(mount.callCount, 1);
634 | assert.strictEqual(mount.calledWith(null, vNode), true);
635 | assert.strictEqual(mount.calledAfter(init), true);
636 | });
637 |
638 | it('mount in nested component', () => {
639 | const vNode = h(C, {children: h(P)});
640 | r(vNode);
641 | assert.strictEqual(pMount.callCount, 1);
642 | });
643 |
644 | it('mount component in element', () => {
645 | const vNode = h('div', null, h(P));
646 | r(vNode);
647 | assert.strictEqual(pMount.callCount, 1);
648 | });
649 |
650 | it('mount manually', () => {
651 | const mountedQueue = new MountedQueue();
652 | const vNode = h(C);
653 | reset();
654 | const dom = render(vNode, null, mountedQueue);
655 | container.appendChild(dom);
656 | mountedQueue.trigger();
657 |
658 | assert.strictEqual(mount.callCount, 1);
659 | });
660 | });
661 |
662 | describe('SVG', () => {
663 | if (isIE8) return;
664 |
665 | it('render svg', () => {
666 | const vNode = h('svg', null, h('circle', {
667 | cx: 100,
668 | cy: 50,
669 | r: 40,
670 | stroke: 'black',
671 | 'stroke-width': 2,
672 | fill: 'red'
673 | }));
674 | r(vNode);
675 | assert.strictEqual(container.firstChild.namespaceURI, svgNS);
676 | assert.strictEqual(container.firstChild.firstChild.namespaceURI, svgNS);
677 | });
678 |
679 | it('render svg component', () => {
680 | class SvgComponent {
681 | constructor(props) {
682 | this.props = props;
683 | }
684 |
685 | init() {
686 | return render(h('circle', {
687 | cx: 50,
688 | cy: 50,
689 | r: 50,
690 | fill: 'red'
691 | }), null, null, null, this.isSVG);
692 | }
693 | }
694 | r(h('svg', null, h(SvgComponent)));
695 | assert.strictEqual(container.firstChild.firstChild.namespaceURI, svgNS);
696 | });
697 |
698 | // android 4.4 does not support
699 | // it('svg set event', (done) => {
700 | // if (!browser.isChrome) return done();
701 | // const vNode = h('svg', null, h('circle', {
702 | // cx: 100,
703 | // cy: 50,
704 | // r: 40,
705 | // stroke: 'black',
706 | // 'stroke-width': 2,
707 | // fill: 'red'
708 | // }, h('set', {
709 | // attributeName: 'fill',
710 | // to: 'blue',
711 | // begin: 'click'
712 | // })));
713 | // r(vNode);
714 | // dispatchEvent(container.firstChild.firstChild, 'click');
715 | // setTimeout(() => {
716 | // assert.strictEqual(
717 | // getComputedStyle(container.firstChild.firstChild).fill,
718 | // 'rgb(0, 0, 255)'
719 | // );
720 | // done();
721 | // });
722 | // });
723 |
724 | it('attach event listener', () => {
725 | const fn = sinon.spy();
726 | const vNode = h('svg', {'ev-click': fn});
727 | r(vNode);
728 | dispatchEvent(container.firstChild, 'click');
729 | assert.strictEqual(fn.callCount, 1);
730 | assert.strictEqual(fn.args[0].length, 1);
731 | assert.strictEqual(fn.args[0][0].type, 'click');
732 | assert.strictEqual(fn.args[0][0].target, container.firstChild);
733 | assert.strictEqual(fn.args[0][0].currentTarget, container.firstChild);
734 |
735 | dispatchEvent(container.firstChild, 'click');
736 | assert.strictEqual(fn.callCount, 2);
737 | });
738 |
739 | });
740 | });
741 |
--------------------------------------------------------------------------------
/test/tostring.js:
--------------------------------------------------------------------------------
1 | import {toString} from '../src/tostring';
2 | import {h, hc, render} from '../src';
3 | import assert from 'assert';
4 |
5 | function eql(vNode, html) {
6 | assert.strictEqual(toString(vNode), html);
7 | }
8 |
9 | class ClassComponent {
10 | constructor(props) {
11 | this.props = props || {};
12 | }
13 | init() {}
14 | toString() {
15 | this.vNode = h('span', this.props, this.props.children);
16 | return toString(this.vNode);
17 | }
18 | }
19 |
20 | describe('toString', () => {
21 | it('render element to string', () => {
22 | eql(h('div'), '');
23 | eql(h('div', {a: 1, b: 'b'}), '');
24 | eql(h('div', {a: 1}, 'test'), 'test
');
25 | eql(h('div', null, h('i'), 'test'), '
');
26 | eql(
27 | h('div', null, [h('i'), h('b', {a: 'a'})], "test"),
28 | '
'
29 | );
30 | });
31 |
32 | it('render style to string', () => {
33 | eql(
34 | h('div', {style: 'font-size: 14px; color: red;'}),
35 | ''
36 | );
37 |
38 | eql(
39 | h('div', {style: {fontSize: '14px', color: 'red'}}),
40 | ''
41 | );
42 | });
43 |
44 | it('render attributes to string', () => {
45 | eql(
46 | h('div', {attributes: {a: 1, b: '2', c: 'c', checked: true}}),
47 | ''
48 | );
49 | });
50 |
51 | it('render dataset to string', () => {
52 | eql(
53 | h('div', {dataset: {a: '1', 'aA': true, 'aAA': 3}}),
54 | ''
55 | );
56 | });
57 |
58 | it('render innerHTML to string', () => {
59 | eql(
60 | h('div', {innerHTML: '', a: 1}),
61 | '
'
62 | );
63 | });
64 |
65 | it('render defaultValue and defaultChecked to string', () => {
66 | eql(
67 | h('input', {defaultValue: 0}),
68 | ''
69 | );
70 | eql(
71 | h('input', {defaultValue: '1', value: 0}),
72 | ''
73 | );
74 | eql(
75 | h('input', {defaultChecked: true}),
76 | ''
77 | );
78 | eql(
79 | h('input', {defaultChecked: true, checked: false}),
80 | ''
81 | );
82 | });
83 |
84 | it('render