├── .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 | Sauce Test Status 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 += ``; 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 | '
\r\n
' 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 | '
\r\n
' 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 | '
\r\n

' 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 | '
\r\n

' 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 | '
\r\n
' 545 | ); 546 | eql( 547 | h('div', null, h(ClassComponent)), 548 | h('div', null, h(C, {className: 'a'})), 549 | '
', 550 | '
\r\n
' 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 | '
a
0
1
2
3testtextb
', 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, '
changed
test
'); 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, '
changed
test
'); 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 | '
\r\n
' 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 | '
text
', 145 | '
text\r\n
' 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 | '
text
', 164 | '
text\r\n
' 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 | '
\r\n

' 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 | '
\r\n

' 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 | '
\r\n
' 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