├── .npmignore ├── src ├── Markers │ ├── Text.js │ ├── Reference.js │ ├── List.js │ ├── EventListener.js │ ├── ClassList.js │ ├── Context.js │ ├── Property.js │ ├── Template │ │ ├── IndexedTemplate.js │ │ └── Template.js │ ├── Link.js │ └── Foreach.js ├── Expression │ ├── Flags.js │ ├── Expression.js │ └── Path.js └── Engine.js ├── .travis.yml ├── linked-html.js ├── .jshintrc ├── .gitignore ├── webpack.config.js ├── bower.json ├── test ├── Markers │ ├── Text.js │ ├── EventListener.js │ ├── ClassList.js │ ├── Property.js │ ├── Context.js │ ├── Link.js │ └── Foreach.js ├── Engine.js └── Expression │ ├── Path.js │ └── Expression.js ├── karma.conf.js ├── LICENSE ├── README.md ├── package.json └── dist └── linked-html.js /.npmignore: -------------------------------------------------------------------------------- 1 | # Tests 2 | /test 3 | .travis.yml 4 | karma.conf.js 5 | 6 | # Dev 7 | .jshintrc 8 | webpack.config.js 9 | 10 | # Other 11 | bower.json 12 | .gitignore 13 | -------------------------------------------------------------------------------- /src/Markers/Text.js: -------------------------------------------------------------------------------- 1 | import Property from './Property'; 2 | 3 | export default function Text(engine, node, evaluate) { 4 | Property(engine, node, `textContent: ${evaluate}`); 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | addons: 5 | firefox: latest 6 | before_install: 7 | - "export DISPLAY=:99.0" 8 | - "sh -e /etc/init.d/xvfb start" 9 | - sleep 3 10 | -------------------------------------------------------------------------------- /src/Markers/Reference.js: -------------------------------------------------------------------------------- 1 | import Expression from '../Expression/Expression'; 2 | 3 | export default function Reference(engine, node, evaluate) { 4 | const expr = new Expression(engine, evaluate); 5 | expr.set(node, true); 6 | } 7 | -------------------------------------------------------------------------------- /linked-html.js: -------------------------------------------------------------------------------- 1 | // Main modules 2 | export { default as Engine } from './src/Engine'; 3 | export { default as Expression } from './src/Expression/Expression'; 4 | 5 | // Tools 6 | export { register as register } from './src/Component'; 7 | -------------------------------------------------------------------------------- /src/Markers/List.js: -------------------------------------------------------------------------------- 1 | export function list(evaluate, cb) { 2 | evaluate.split(';').forEach(v => { 3 | const [name, value] = v.split(':'); 4 | 5 | if (!name || !value) { 6 | throw new SyntaxError(`'${v}': Invalid list element.`); 7 | } 8 | 9 | cb(name.trim(), value.trim()); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/Markers/EventListener.js: -------------------------------------------------------------------------------- 1 | import Expression from '../Expression/Expression'; 2 | import {list} from './List'; 3 | 4 | export default function EventListener(engine, node, evaluate) { 5 | list(evaluate, (name, value)=> { 6 | const expr = new Expression(engine, value); 7 | node.addEventListener(name, (e)=> expr.call(e)); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "indent": 2, 7 | "maxlen": 80, 8 | "noarg": true, 9 | "notypeof": true, 10 | "undef": true, 11 | "unused": true, 12 | "latedef": "nofunc", 13 | 14 | "expr": true, 15 | 16 | "browser": true, 17 | "esnext": true, 18 | "jasmine": true, 19 | "node": true 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | .cache 16 | .project 17 | .settings 18 | .tmproj 19 | nbproject 20 | Thumbs.db 21 | 22 | # NPMs 23 | node_modules/ 24 | npm-debug.log 25 | 26 | # Test 27 | coverage/ 28 | .coveralls.yml 29 | test/performance.js 30 | -------------------------------------------------------------------------------- /src/Expression/Flags.js: -------------------------------------------------------------------------------- 1 | const Flags = { 2 | '!': function(expr) { 3 | const filter = expr.filter; 4 | expr.filter = { 5 | get: v => filter.get(!v), set: v => filter.set(!v) 6 | }; 7 | }, 8 | '@': function(expr, engine) { 9 | expr.context = ()=> engine; 10 | }, 11 | '&': function(expr) { 12 | expr.set = ()=> {}; 13 | }, 14 | '*': function(expr) { 15 | expr.deep = true; 16 | } 17 | }; 18 | 19 | export default Flags; 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: "./linked-html", 5 | output: { 6 | path: "./dist/", 7 | libraryTarget: "umd", 8 | library: "LinkedHtml", 9 | filename: "linked-html.js" 10 | }, 11 | module: { 12 | loaders: [ 13 | { test: /\.js$/, loader: 'babel?presets[]=es2015' } 14 | ] 15 | }, 16 | plugins: [ 17 | new webpack.optimize.UglifyJsPlugin({compress: { warnings: false }}) 18 | ], 19 | devtool: '#source-map' 20 | }; 21 | -------------------------------------------------------------------------------- /src/Markers/ClassList.js: -------------------------------------------------------------------------------- 1 | import Expression from '../Expression/Expression'; 2 | import {list} from './List'; 3 | 4 | export default function ClassList(engine, node, evaluate) { 5 | list(evaluate, (name, value)=> { 6 | const expr = new Expression(engine, value); 7 | 8 | expr.set(node.classList.contains(name), true); 9 | expr.observe(val => { 10 | if (val) { 11 | node.classList.add(name); 12 | } else { 13 | node.classList.remove(name); 14 | } 15 | }, true, false); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linked-html", 3 | "version": "0.3.1", 4 | "homepage": "https://github.com/smalluban/linked-html", 5 | "authors": [ 6 | "Dominik Lubański " 7 | ], 8 | "description": "A multi-purpose HTML & data linking library", 9 | "main": "dist/linked-html.js", 10 | "moduleType": [ 11 | "amd", 12 | "es6", 13 | "globals" 14 | ], 15 | "keywords": [ 16 | "rendering", 17 | "data binding", 18 | "parsing" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | "node_modules", 23 | "test" 24 | ], 25 | "dependencies": { 26 | "papillon": "~1.5.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/Markers/Text.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../src/Engine'; 2 | import Text from '../../src/Markers/Text'; 3 | 4 | describe('Text', ()=> { 5 | let engine, node; 6 | 7 | beforeEach(()=> { 8 | node = document.createElement('div'); 9 | node.textContent = 'This is simple text'; 10 | engine = new Engine(document.createElement('div')); 11 | Text(engine, node, 'test'); 12 | }); 13 | 14 | it('get textContent property from node', ()=> { 15 | expect(engine.state.test).toEqual('This is simple text'); 16 | }); 17 | 18 | it('set textContent property to node', (done)=> { 19 | engine.state.test = 'My title'; 20 | 21 | window.requestAnimationFrame(()=> { 22 | expect(node.textContent).toEqual('My title'); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/Markers/Context.js: -------------------------------------------------------------------------------- 1 | import Template from './Template/Template'; 2 | import Expression from '../Expression/Expression'; 3 | 4 | export default function Context(engine, node, evaluate) { 5 | if (!node.children[0]) { 6 | throw new Error('No children elements.'); 7 | } 8 | 9 | const template = new Template( 10 | Array.from(node.childNodes) 11 | .filter(n => n.nodeType === Node.ELEMENT_NODE), 12 | engine 13 | ); 14 | 15 | const expr = new Expression(engine, evaluate); 16 | 17 | expr.set({}, true); 18 | expr.observe(state => { 19 | if (!state) { 20 | template.remove(); 21 | } else { 22 | if (typeof state !== 'object') { 23 | throw new TypeError('Invalid context target.'); 24 | } 25 | template.setState(state).append(); 26 | } 27 | }, true, true); 28 | } 29 | 30 | Context._options = { 31 | breakCompile: true 32 | }; 33 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['jasmine'], 5 | files: [ 6 | 'node_modules/core-js/client/shim.js', 7 | 'node_modules/document-register-element/build/document-register-element.js', // jshint ignore:line 8 | { pattern: 'test/**/*.js', watched: false } 9 | ], 10 | exclude: [], 11 | preprocessors: { 12 | 'test/**/*.js': ['webpack'] 13 | }, 14 | webpack: { 15 | module: { 16 | loaders: [ 17 | { test: /\.js$/, loader: 'babel?presets[]=es2015' } 18 | ] 19 | }, 20 | devtool: "#inline-source-map" 21 | }, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | reporters: ['progress'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /test/Markers/EventListener.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../src/Engine'; 2 | import EventListener from '../../src/Markers/EventListener'; 3 | 4 | describe('EventListener', ()=> { 5 | let engine, node, spy; 6 | 7 | beforeEach(()=> { 8 | spy = jasmine.createSpy('callback'); 9 | node = document.createElement('ul'); 10 | engine = new Engine(document.createElement('div')); 11 | engine.test = spy; 12 | }); 13 | 14 | beforeEach(()=> { 15 | document.body.appendChild(node); 16 | }); 17 | 18 | afterEach(()=> { 19 | document.body.removeChild(node); 20 | }); 21 | 22 | it('call function on proper event', ()=> { 23 | EventListener(engine, node, 'click: @test'); 24 | 25 | const event = document.createEvent('Event'); 26 | event.initEvent('click', true, false); 27 | node.dispatchEvent(event); 28 | 29 | expect(spy).toHaveBeenCalled(); 30 | expect(spy.calls.mostRecent().object).toEqual(engine); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/Markers/Property.js: -------------------------------------------------------------------------------- 1 | import Expression from '../Expression/Expression'; 2 | import {list} from './List'; 3 | 4 | export default function Property(engine, node, evaluate) { 5 | list(evaluate, (name, value)=> { 6 | const accessor = resolveProperty(node, name); 7 | const expr = new Expression(engine, value); 8 | 9 | expr.set(accessor.get(), true); 10 | expr.observe(accessor.set, true); 11 | }); 12 | } 13 | 14 | function resolveProperty(node, name) { 15 | if (name in node) { 16 | return { 17 | get: ()=> node[name], 18 | set: (value)=> node[name] = value 19 | }; 20 | } else { 21 | return { 22 | get: ()=> { 23 | const val = node.getAttribute(name); 24 | return val ? val : undefined; 25 | }, 26 | set: (value)=> { 27 | if (value === false || value === undefined || value === null) { 28 | node.removeAttribute(name); 29 | } else { 30 | node.setAttribute(name, value === true ? name : value); 31 | } 32 | } 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Markers/Template/IndexedTemplate.js: -------------------------------------------------------------------------------- 1 | import Template from './Template'; 2 | 3 | const map = new WeakMap(); 4 | 5 | function extendEngine(engine) { 6 | if (map.has(engine)) { 7 | return map.get(engine); 8 | } else { 9 | const extended = Object.defineProperties(Object.create(engine), { 10 | number: { get: function() { return this.index + 1; }}, 11 | odd: { get: function() { return this.number % 2 !== 0; }}, 12 | even: { get: function() { return this.number % 2 === 0; }}, 13 | first: { get: function() { return this.index === 0; }}, 14 | last: { get: function() { return this.length === this.number; }} 15 | }); 16 | 17 | map.set(engine, extended); 18 | 19 | return extended; 20 | } 21 | } 22 | 23 | export default class IndexedTemplate extends Template { 24 | constructor(nodes, engine, host) { 25 | super(nodes, extendEngine(engine), host); 26 | } 27 | 28 | setState(state, {index, length}) { 29 | super.setState(state); 30 | Object.assign(this.engine, {index, length}); 31 | 32 | return this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dominik Lubański 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linked HTML [![Build Status](https://travis-ci.org/smalluban/linked-html.svg?branch=master)](https://travis-ci.org/smalluban/linked-html) 2 | 3 | Linked HTML is a library for creating dynamic user interfaces from static html content. 4 | 5 | * **Fastboot data model:** Initial data model is extracted from static html generated by any server-side language or template engine. 6 | * **Safe and simple syntax:** Links between html and data model are marked only by custom attributes. This ensures no conflict with template engines syntax. 7 | * **Data binding**: Library uses one-way data binding with flexible control over primitive values, arrays and objects. 8 | * **Superfast rendering**: Initial state is pre-rendered - bootstrapping library usually takes no DOM changes. Mutated state re-use existing html nodes as much as possible. 9 | * **Small and simple API**: The architecture covers only three simple concepts. 10 | 11 | ## Documentation 12 | The latest documentation is available at project [Wiki](https://github.com/smalluban/linked-html/wiki). 13 | 14 | ## Contribute 15 | Feel free to contribute! Fork project, install node dependencies and run: 16 | 17 | ``` 18 | npm run develop 19 | ``` 20 | 21 | Please provide tests before creating pull request. 22 | 23 | ### License 24 | Linked HTML is released under the [MIT License](https://github.com/smalluban/linked-html/blob/master/LICENSE). 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linked-html", 3 | "version": "0.3.1", 4 | "description": "A multi-purpose HTML & data linking library", 5 | "main": "dist/linked-html.js", 6 | "scripts": { 7 | "test": "./node_modules/karma/bin/karma start --single-run --browsers Firefox", 8 | "develop": "./node_modules/karma/bin/karma start", 9 | "build": "webpack", 10 | "prebuild": "npm test", 11 | "prepublish": "npm run build" 12 | }, 13 | "author": "Dominik Lubański ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/smalluban/linked-html/issues" 17 | }, 18 | "keywords": [ 19 | "rendering", 20 | "data binding", 21 | "parsing" 22 | ], 23 | "homepage": "https://github.com/smalluban/linked-html", 24 | "dependencies": { 25 | "papillon": "^1.5.2" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.1.4", 29 | "babel-loader": "^6.1.0", 30 | "babel-preset-es2015": "^6.1.4", 31 | "core-js": "^1.2.2", 32 | "document-register-element": "^0.5.3", 33 | "jasmine-core": "^2.3.4", 34 | "karma": "^0.13.3", 35 | "karma-chrome-launcher": "^0.2.0", 36 | "karma-firefox-launcher": "^0.1.6", 37 | "karma-ievms": "^0.1.0", 38 | "karma-jasmine": "^0.3.6", 39 | "karma-safari-launcher": "^0.1.1", 40 | "karma-webpack": "^1.7.0", 41 | "webpack": "^1.11.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Markers/ClassList.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../src/Engine'; 2 | import ClassList from '../../src/Markers/ClassList'; 3 | 4 | describe('ClassList', ()=> { 5 | let engine, node; 6 | 7 | beforeEach(()=> { 8 | node = document.createElement('div'); 9 | engine = new Engine(document.createElement('div')); 10 | }); 11 | 12 | it('set state property to `true`', ()=> { 13 | node.className = 'myClass'; 14 | ClassList(engine, node, 'myClass: test'); 15 | expect(engine.state.test).toEqual(true); 16 | }); 17 | 18 | it('set state property to `false`', ()=> { 19 | ClassList(engine, node, 'myClass: test'); 20 | expect(engine.state.test).toEqual(false); 21 | }); 22 | 23 | it('set `myClass` class', (done)=> { 24 | ClassList(engine, node, 'myClass: test'); 25 | engine.state.test = true; 26 | 27 | window.requestAnimationFrame(()=> { 28 | expect(node.classList.contains('myClass')).toEqual(true); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('unset `myClass` class', (done)=> { 34 | node.className = 'myClass'; 35 | ClassList(engine, node, 'myClass: test'); 36 | engine.state.test = false; 37 | 38 | window.requestAnimationFrame(()=> { 39 | expect(node.classList.contains('myClass')).toEqual(false); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('use class list', ()=> { 45 | node.setAttribute('class', 'a b c'); 46 | ClassList(engine, node, 'a: test1; b: test2; d: test3'); 47 | 48 | expect(engine.state).toEqual({ 49 | test1: true, 50 | test2: true, 51 | test3: false 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/Markers/Template/Template.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../Engine'; 2 | 3 | export default class Template { 4 | constructor(nodes, engine, host) { 5 | this.nodes = [].concat(nodes); 6 | this.host = host || this.nodes[0].parentElement; 7 | this.parentEngine = engine; 8 | 9 | if (!this.host) { 10 | throw new Error('Invalid host for template.'); 11 | } 12 | 13 | if (!this.nodes[0].parentElement) { 14 | this.hidden = true; 15 | } 16 | } 17 | 18 | getState() { 19 | if (!this.engine) { 20 | throw new Error('Template does not fully initalized.'); 21 | } 22 | 23 | return this.engine.state; 24 | } 25 | 26 | setState(state) { 27 | if (!this.engine) { 28 | this.engine = Engine.spawn(this.parentEngine, this.nodes, state); 29 | } else { 30 | this.engine.state = state; 31 | } 32 | 33 | return this; 34 | } 35 | 36 | append(forceAppend = false, insertBefore = null) { 37 | if (this.hidden || forceAppend) { 38 | this.nodes.forEach(node => { 39 | this.host.insertBefore(node, insertBefore); 40 | }); 41 | this.hidden = false; 42 | } 43 | 44 | return this; 45 | } 46 | 47 | remove() { 48 | if (!this.hidden) { 49 | const fragment = document.createDocumentFragment(); 50 | this.nodes.forEach(node => fragment.appendChild(node)); 51 | this.hidden = true; 52 | } 53 | 54 | return this; 55 | } 56 | 57 | clone() { 58 | return new this.constructor( 59 | this.nodes.map(node => node.cloneNode(true)), 60 | this.parentEngine, 61 | this.host 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/Markers/Property.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../src/Engine'; 2 | import Property from '../../src/Markers/Property'; 3 | 4 | describe('Property', ()=> { 5 | let node, engine; 6 | 7 | beforeEach(()=> { 8 | node = document.createElement('div'); 9 | node.title = 'test title'; 10 | node.id = 'test-id'; 11 | engine = new Engine(document.createElement('div')); 12 | }); 13 | 14 | it('set engine state path from node property', ()=> { 15 | Property(engine, node, 'title: test'); 16 | expect(engine.state.test).toEqual('test title'); 17 | }); 18 | 19 | it('set node property from engine state path', ()=> { 20 | engine.state.test = 'new value'; 21 | Property(engine, node, 'title: test'); 22 | expect(node.title).toEqual('new value'); 23 | }); 24 | 25 | it('set node property from engine state', (done)=> { 26 | Property(engine, node, 'title: test'); 27 | engine.state.test = 'test'; 28 | 29 | window.requestAnimationFrame(()=> { 30 | expect(node.title).toEqual('test'); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('set list of properties', ()=> { 36 | Property(engine, node, 'title: test1; id: test2'); 37 | expect(engine.state).toEqual({ 38 | test1: 'test title', 39 | test2: 'test-id' 40 | }); 41 | }); 42 | 43 | describe('fallback', ()=> { 44 | 45 | it('as attribute value', ()=> { 46 | engine.state.test = 'test'; 47 | Property(engine, node, 'some-attr: test'); 48 | expect(node.getAttribute('some-attr')).toEqual('test'); 49 | }); 50 | 51 | it('as attribute name for true value', ()=> { 52 | engine.state.test = true; 53 | Property(engine, node, 'some-attr: test'); 54 | 55 | expect(node.getAttribute('some-attr')).toEqual('some-attr'); 56 | }); 57 | 58 | it('as removed attribute for false value', ()=> { 59 | engine.state.test = false; 60 | Property(engine, node, 'some-attr: test'); 61 | 62 | expect(node.getAttribute('some-attr')).toEqual(null); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/Markers/Context.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../src/Engine'; 2 | import Context from '../../src/Markers/Context'; 3 | 4 | describe('Context', ()=> { 5 | let node, engine; 6 | 7 | beforeEach(()=> { 8 | node = document.createElement('div'); 9 | node.innerHTML = `
MyTitle
`; 10 | engine = new Engine(document.createElement('div')); 11 | }); 12 | 13 | it('create context for nested markers', ()=> { 14 | Context(engine, node, 'test'); 15 | expect(engine.state).toEqual({ 16 | test: { title: 'MyTitle' } 17 | }); 18 | }); 19 | 20 | it('updates nested properties', (done)=> { 21 | Context(engine, node, 'test'); 22 | engine.state.test = { title: 'new Title' }; 23 | window.requestAnimationFrame(()=> { 24 | expect(node.children[0].textContent).toEqual('new Title'); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('unmounts children for falsy context value', (done)=> { 30 | engine.state.test = false; 31 | Context(engine, node, 'test'); 32 | window.requestAnimationFrame(()=> { 33 | expect(node.childNodes.length).toEqual(0); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('unmounts children for falsy context from state change', (done)=> { 39 | Context(engine, node, 'test'); 40 | engine.state.test = ''; 41 | 42 | window.requestAnimationFrame(()=> { 43 | expect(node.childNodes.length).toEqual(0); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('mounts children for object context', (done)=> { 49 | Context(engine, node, 'test'); 50 | engine.state.test = { title: 'SuperTitle' }; 51 | 52 | window.requestAnimationFrame(()=> { 53 | expect(node.children[0].textContent).toEqual('SuperTitle'); 54 | done(); 55 | }); 56 | }); 57 | 58 | describe('for multiply context', ()=> { 59 | beforeEach(()=> { 60 | node.innerHTML = ` 61 |
62 |
MyTitle
63 |
64 | `; 65 | Context(engine, node, 'test'); 66 | }); 67 | 68 | it('create nested state', ()=> { 69 | expect(engine.state).toEqual({ 70 | test: { inner: { title: 'MyTitle' } } 71 | }); 72 | }); 73 | 74 | it('update nested state', (done)=> { 75 | engine.state.test.inner.title = 'New title'; 76 | 77 | window.requestAnimationFrame(()=> { 78 | expect(node.children[0].children[0].textContent).toEqual('New title'); 79 | done(); 80 | }); 81 | }); 82 | }); 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /test/Engine.js: -------------------------------------------------------------------------------- 1 | import Engine from '../src/Engine'; 2 | 3 | describe('Engine instance', ()=> { 4 | 5 | let el, marker; 6 | 7 | beforeEach(()=> { 8 | marker = jasmine.createSpy('marker'); 9 | el = document.createElement('div'); 10 | el.innerHTML = ` 11 |
12 | To jest text 13 | 14 |
15 | `; 16 | }); 17 | 18 | it('call markers', ()=> { 19 | new Engine(el, { markers: { marker } }); 20 | expect(marker.calls.count()).toEqual(2); 21 | }); 22 | 23 | it('use array as node root', ()=> { 24 | const nodes = [el.children[0].children[0]]; 25 | new Engine(nodes, { markers: { marker } }); 26 | expect(marker.calls.count()).toEqual(1); 27 | }); 28 | 29 | it('use array like object as node root', ()=> { 30 | new Engine(el.children, { markers: { marker } }); 31 | expect(marker.calls.count()).toEqual(2); 32 | }); 33 | 34 | it('use documentFragment as node root', ()=> { 35 | const fragment = document.createDocumentFragment(); 36 | for (let child of Array.from(el.childNodes)) { 37 | fragment.appendChild(child); 38 | } 39 | new Engine(fragment, { markers: { marker } }); 40 | expect(marker.calls.count()).toEqual(2); 41 | }); 42 | 43 | it('call marker with proper arguments', ()=> { 44 | const engine = new Engine(el, { markers: { marker } }); 45 | 46 | expect(marker.calls.argsFor(0)) 47 | .toEqual([engine, el.children[0], "test1"]); 48 | }); 49 | 50 | it('use custom prefix', ()=> { 51 | el.innerHTML = `
`; 52 | const markers = { marker: ()=> {} }; 53 | const spy = spyOn(markers, 'marker'); 54 | 55 | new Engine(el, { 56 | prefix: 'data-custom', 57 | markers 58 | }); 59 | 60 | expect(spy).toHaveBeenCalled(); 61 | }); 62 | 63 | it('translate marker id from dash to camelCase', ()=> { 64 | el.innerHTML = `
`; 65 | const myMarker = jasmine.createSpy('myMarker'); 66 | new Engine(el, { markers: { myMarker }}); 67 | 68 | expect(myMarker).toHaveBeenCalled(); 69 | }); 70 | 71 | it('stop compiling childs when marker has breakCompile option', ()=> { 72 | const Marker = function () {}; 73 | Marker._options = { breakCompile: true }; 74 | const markers = { 75 | marker: Marker 76 | }; 77 | 78 | const spy = spyOn(markers, 'marker').and.callThrough(); 79 | 80 | new Engine(el, { markers }); 81 | expect(spy.calls.count()).toEqual(1); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/Expression/Expression.js: -------------------------------------------------------------------------------- 1 | import { State } from 'papillon/papillon'; 2 | import Engine from '../Engine'; 3 | import Path from './Path'; 4 | import Flags from './Flags'; 5 | 6 | export default class Expression { 7 | static parse(evaluate) { 8 | const [temp, filter] = evaluate.split('|'); 9 | const flags = new Set(); 10 | 11 | for(var index = 0; index < temp.length; index++) { 12 | if (Flags[temp[index]]) { 13 | flags.add(temp[index]); 14 | } else { 15 | break; 16 | } 17 | } 18 | 19 | const expr = temp.substr(index); 20 | return [flags, expr, filter]; 21 | } 22 | 23 | constructor(engine, evaluate) { 24 | if(!evaluate || typeof evaluate !== 'string') { 25 | throw new TypeError(`'${evaluate}': Invalid input type.`); 26 | } 27 | 28 | const [flags, expr, filter] = Expression.parse(evaluate.trim()); 29 | this.engine = engine; 30 | this.filter = { get: v => v, set: v => v }; 31 | 32 | if (this.engine.state) { 33 | this.context = ()=> this.engine.state; 34 | } else { 35 | this.context = ()=> { 36 | if (this.engine.state === undefined) { 37 | this.engine.state = {}; 38 | } 39 | return this.engine.state; 40 | }; 41 | } 42 | 43 | if (filter) { 44 | const filters = Engine.config(engine).filters; 45 | if (!filters[filter]) { 46 | throw new ReferenceError(`Filter '${filter}' not found.`); 47 | } 48 | Object.assign(this.filter, filters[filter]); 49 | } 50 | 51 | flags.forEach(f => Flags[f](this, engine)); 52 | this.path = new Path(expr, this.context); 53 | } 54 | 55 | get() { 56 | return this.filter.get(this.path.get()); 57 | } 58 | 59 | set(value, onlyDefaults) { 60 | return this.path.set(this.filter.set(value), onlyDefaults); 61 | } 62 | 63 | call(...args) { 64 | return this.path.call(...args); 65 | } 66 | 67 | observe(cb, init = false, deep = this.deep) { 68 | let target; 69 | let cache = this.get(); 70 | 71 | if (deep) { 72 | target = new State(Object.defineProperty({}, 'value', { 73 | get: this.get.bind(this), 74 | configurable: true, 75 | enumerable: true 76 | })); 77 | } 78 | 79 | Engine.watch(this.engine, ()=> { 80 | const newVal = this.get(); 81 | 82 | if (target && target.isChanged()) { 83 | cb(newVal, target.changelog.value); 84 | } else if (!State.is(newVal, cache)) { 85 | cb(newVal); 86 | } 87 | 88 | cache = newVal; 89 | }); 90 | 91 | if (init) { 92 | cb(cache); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Markers/Link.js: -------------------------------------------------------------------------------- 1 | import Expression from '../Expression/Expression'; 2 | 3 | class Wrapper { 4 | constructor(node) { 5 | this.node = node; 6 | } 7 | 8 | get() { 9 | return this.node.value; 10 | } 11 | 12 | set(value) { 13 | this.value = value; 14 | 15 | if (this.node.value !== value) { 16 | this.node.value = value; 17 | } 18 | } 19 | 20 | observe(cb) { 21 | this.node.addEventListener('input', cb); 22 | } 23 | } 24 | 25 | class CheckWrapper extends Wrapper { 26 | get() { 27 | return this.node.checked ? this.node.value : undefined; 28 | } 29 | 30 | set(value) { 31 | if (this.node.value === value) { 32 | this.node.checked = true; 33 | } else { 34 | this.node.checked = false; 35 | } 36 | } 37 | 38 | observe(cb) { 39 | this.node.addEventListener('change', cb); 40 | } 41 | } 42 | 43 | class SelectWrapper extends Wrapper { 44 | constructor(node) { 45 | super(node); 46 | 47 | if ('MutationObserver' in window) { 48 | new MutationObserver(()=> { 49 | this.set(this.value); 50 | }).observe(node, { 51 | attributes: true, 52 | childList: true, 53 | characterData: true, 54 | subtree: true 55 | }); 56 | } 57 | } 58 | 59 | set(value) { 60 | this.value = value; 61 | 62 | Array.from(this.node.options).some(o => { 63 | if (o.value === value) { 64 | o.selected = true; 65 | return true; 66 | } 67 | }); 68 | } 69 | 70 | observe(cb) { 71 | this.node.addEventListener('change', cb); 72 | } 73 | } 74 | 75 | class MultiSelectWrapper extends SelectWrapper { 76 | get() { 77 | const values = this.value || []; 78 | const newValues = Array.from(this.node.options) 79 | .filter(o => o.selected) 80 | .map(o => o.value); 81 | 82 | Object.assign(values, newValues); 83 | values.length = newValues.length; 84 | 85 | return values; 86 | } 87 | 88 | set(values) { 89 | this.value = values; 90 | 91 | if (!Array.isArray(values)) { 92 | throw new Error('Invalid values. Array instance required.'); 93 | } 94 | 95 | Array.from(this.node.options).forEach(o => { 96 | o.selected = values.some(v => v === o.value); 97 | }); 98 | } 99 | } 100 | 101 | export default function Link(engine, node, evaluate) { 102 | let wrapper; 103 | 104 | switch(node.type) { 105 | case "checkbox": 106 | case "radio": 107 | wrapper = new CheckWrapper(node); 108 | break; 109 | 110 | case "select-one": 111 | wrapper = new SelectWrapper(node); 112 | break; 113 | 114 | case "select-multiple": 115 | wrapper = new MultiSelectWrapper(node); 116 | break; 117 | 118 | default: 119 | wrapper = new Wrapper(node); 120 | } 121 | 122 | const expr = new Expression(engine, evaluate); 123 | 124 | expr.set(wrapper.get(), true); 125 | expr.observe(wrapper.set.bind(wrapper), true, false); 126 | wrapper.observe(()=> expr.set(wrapper.get())); 127 | } 128 | -------------------------------------------------------------------------------- /test/Expression/Path.js: -------------------------------------------------------------------------------- 1 | import Path from '../../src/Expression/Path'; 2 | 3 | describe('Path', ()=> { 4 | let context, obj; 5 | 6 | describe('get method', ()=> { 7 | beforeEach(()=> { 8 | obj = { 9 | one: { two: { three: "four" }}, 10 | arr: [1,{two: ['three']}] 11 | }; 12 | context = ()=> obj; 13 | }); 14 | 15 | it('returns path value', ()=> { 16 | const path1 = new Path('one.two.three', context); 17 | const path2 = new Path('arr[1].two[0]', context); 18 | expect(path1.get()).toEqual('four'); 19 | expect(path2.get()).toEqual('three'); 20 | }); 21 | 22 | it('not create path for undefined property', ()=> { 23 | const path = new Path('two.three.four', context); 24 | expect(path.get()).toEqual(undefined); 25 | expect(context.two).toEqual(undefined); 26 | }); 27 | 28 | it('throws for invalid type of property path', ()=> { 29 | const path = new Path('one.two.three.four', context); 30 | expect(()=> path.get()).toThrow(); 31 | }); 32 | 33 | }); 34 | 35 | describe('set method', ()=> { 36 | beforeEach(()=> { 37 | obj = { asd: {}, qwe: 123 }; 38 | context = ()=> obj; 39 | }); 40 | 41 | it('create property path', ()=> { 42 | let path = new Path('asd.dsa.qwe', context); 43 | path.set('new Value'); 44 | 45 | expect(context().asd.dsa.qwe).toEqual('new Value'); 46 | }); 47 | 48 | it('not create property path when already set', ()=> { 49 | let path = new Path('qwe', context); 50 | path.set('new Value', true); 51 | 52 | expect(context().qwe).toEqual(123); 53 | }); 54 | 55 | it('set property path when force', ()=> { 56 | let path = new Path('qwe', context); 57 | path.set('new Value'); 58 | 59 | expect(context().qwe).toEqual('new Value'); 60 | }); 61 | 62 | it('throws for invalid type of property path', ()=> { 63 | let path = new Path('qwe.value', context); 64 | expect(()=> path.set('test')).toThrow(); 65 | }); 66 | }); 67 | 68 | describe('call method', ()=> { 69 | let path, obj, spy; 70 | 71 | beforeEach(()=> { 72 | spy = jasmine.createSpy('callback'); 73 | obj = { 74 | a: 'string', 75 | b: spy 76 | }; 77 | }); 78 | 79 | it('throws for not function', ()=> { 80 | path = new Path('a', ()=> obj); 81 | const fn = ()=> path.call(); 82 | expect(fn).toThrow(); 83 | }); 84 | 85 | it('calls function with proper context', ()=> { 86 | path = new Path('b', ()=> obj); 87 | path.call('test1', 'test2'); 88 | expect(spy.calls.mostRecent().object).toEqual(obj); 89 | expect(spy.calls.mostRecent().args).toEqual(['test1', 'test2']); 90 | }); 91 | }); 92 | 93 | describe('delete method', ()=> { 94 | beforeEach(()=> { 95 | const obj = { 96 | a: { b: { c: 'test' } }, 97 | e: { 98 | f: 'test', 99 | g: { h: 'test' } 100 | } 101 | }; 102 | context = ()=> obj; 103 | }); 104 | 105 | it('removes path with one key in chain', ()=> { 106 | let path = new Path('a.b.c', context); 107 | path.delete(); 108 | 109 | expect(context().a).toBeUndefined(); 110 | }); 111 | 112 | it('not removes path when more keys are in chain', ()=> { 113 | let path = new Path('e.g.h', context); 114 | path.delete(); 115 | 116 | expect(context().e).toEqual({ f: 'test' }); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/Expression/Path.js: -------------------------------------------------------------------------------- 1 | export default class Path { 2 | constructor(evaluate, context) { 3 | if (!evaluate) { 4 | throw new TypeError('Path cannot be empty.'); 5 | } 6 | 7 | this.evaluate = evaluate; 8 | this.context = context; 9 | this.path = this.parse(evaluate); 10 | } 11 | 12 | parse(path) { 13 | let result = []; 14 | 15 | const property = path.split('').reduce( 16 | (acc, char)=> { 17 | switch(char) { 18 | case ".": 19 | result.push({property: acc, proto: Object}); 20 | return ""; 21 | case "[": 22 | result.push({property: acc, proto: Array}); 23 | return ""; 24 | case "]": 25 | return acc; 26 | } 27 | 28 | return acc + char; 29 | }, ""); 30 | 31 | result.push({property}); 32 | 33 | return result; 34 | } 35 | 36 | getContext() { 37 | const context = this.context(); 38 | if (Object(context) !== context) { 39 | throw new TypeError('Object required as path context.'); 40 | } 41 | return context; 42 | } 43 | 44 | get() { 45 | let result = this.getContext(); 46 | 47 | if (result) { 48 | this.path.every(({property, proto})=> { 49 | result = result[property]; 50 | 51 | if(result && proto && !(result instanceof Object)) { 52 | throw new TypeError( 53 | `'${property}' in '${this.evaluate}': Object instance required.`); 54 | } 55 | 56 | return result; 57 | }); 58 | } 59 | 60 | return result; 61 | } 62 | 63 | set(newVal, check = false) { 64 | const {context, property} = this.path.reduce((acc, {property, proto})=> { 65 | if (proto) { 66 | if (!acc.context[property]) { 67 | acc.context[property] = proto(); 68 | } else if(!(acc.context[property] instanceof Object)) { 69 | throw new TypeError( 70 | `'${property}' in '${this.evaluate}': Object instance required.`); 71 | } 72 | 73 | acc.context = acc.context[property]; 74 | } else { 75 | acc.property = property; 76 | } 77 | 78 | return acc; 79 | }, { context: this.getContext() }); 80 | 81 | if (!check || context[property] === undefined) { 82 | context[property] = newVal; 83 | } 84 | 85 | return context[property]; 86 | } 87 | 88 | call(...args) { 89 | const { context, property } = this.path.reduce((acc, {property, proto})=> { 90 | if (proto) { 91 | acc.context = acc.context[property]; 92 | if(!(acc.context instanceof Object)) { 93 | throw new TypeError( 94 | `'${property}' in '${this.evaluate}': Object instance required.`); 95 | } 96 | } else { 97 | acc.property = property; 98 | } 99 | 100 | return acc; 101 | }, { context: this.getContext() }); 102 | 103 | if (typeof context[property] !== 'function') { 104 | throw new TypeError(`'${this.evaluate}': Function required.`); 105 | } 106 | 107 | return context[property](...args); 108 | } 109 | 110 | delete() { 111 | let context = this.getContext(); 112 | let result = { context, property: this.path[0].property }; 113 | 114 | this.path.some(({property}) => { 115 | if (!context.hasOwnProperty(property)) { 116 | return true; 117 | } 118 | 119 | if (Object.keys(context).length > 1) { 120 | result = { context, property }; 121 | } 122 | 123 | context = context[property]; 124 | }); 125 | 126 | delete result.context[result.property]; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Engine.js: -------------------------------------------------------------------------------- 1 | import { State } from 'papillon/papillon'; 2 | 3 | import Link from './Markers/Link'; 4 | import ClassList from './Markers/ClassList'; 5 | import Context from './Markers/Context'; 6 | import EventListener from './Markers/EventListener'; 7 | import Foreach from './Markers/Foreach'; 8 | import Property from './Markers/Property'; 9 | import Reference from './Markers/Reference'; 10 | import Text from './Markers/Text'; 11 | 12 | const Markers = { 13 | link: Link, 14 | class: ClassList, 15 | context: Context, 16 | on: EventListener, 17 | foreach: Foreach, 18 | prop: Property, 19 | ref: Reference, 20 | text: Text 21 | }; 22 | 23 | const Filters = { 24 | int: { set: v => parseInt(v, 10) }, 25 | bool: { set: Boolean } 26 | }; 27 | 28 | const watchers = new WeakMap(); 29 | const configs = new WeakMap(); 30 | const states = new WeakMap(); 31 | 32 | export default class Engine { 33 | static watch(engine, cb) { 34 | let e = watchers.get(engine); 35 | if (!e) { 36 | e = new Set(); 37 | watchers.set(engine, e); 38 | } 39 | e.add(cb); 40 | } 41 | 42 | static config(engine) { 43 | let c = configs.get(engine.root); 44 | if (!c) { 45 | c = {}; 46 | configs.set(engine, c); 47 | } 48 | return c; 49 | } 50 | 51 | static queue(engine) { 52 | if (!this._request) { 53 | State.now(); 54 | 55 | this._engines = new Set().add(engine); 56 | this._request = window.requestAnimationFrame(()=> { 57 | this._engines.forEach(e => { 58 | const set = watchers.get(e); 59 | if (set) { 60 | set.forEach(cb => cb()); 61 | } 62 | }); 63 | this._request = this._engines = undefined; 64 | }); 65 | } else { 66 | this._engines.add(engine); 67 | } 68 | } 69 | 70 | static spawn(engine, nodeList, state) { 71 | const config = Engine.config(engine); 72 | const childEngine = Object.create(engine); 73 | 74 | Object.defineProperty(childEngine, 'parent', { value: engine }); 75 | childEngine.state = state; 76 | 77 | nodeList.forEach( 78 | node => compile(node, childEngine, config.prefix, config.markers) 79 | ); 80 | 81 | return childEngine; 82 | } 83 | 84 | constructor(node, {state, markers, filters, prefix, live} = {}) { 85 | if (!node) { 86 | throw new TypeError('Invalid first argument.'); 87 | } 88 | 89 | if (state && Object(state) !== state) { 90 | throw new TypeError(`Invalid 'state' option, object required.`); 91 | } 92 | 93 | const config = Object.defineProperties(Engine.config(this), { 94 | markers: { value: Object.assign({}, Markers, markers) }, 95 | filters: { value: Object.assign({}, Filters, filters) }, 96 | prefix: { value: (prefix || '-') + '-' }, 97 | live: { value: live === undefined ? true : live } 98 | }); 99 | 100 | Object.defineProperty(this, 'root', { value: this }); 101 | this.state = state || {}; 102 | 103 | if(node.nodeType === Node.ELEMENT_NODE) { 104 | compile(node, this, config.prefix, config.markers); 105 | } else { 106 | Array.from(node.childNodes || node).forEach( 107 | n => { 108 | if (n.nodeType === Node.ELEMENT_NODE) { 109 | compile(n, this, config.prefix, config.markers); 110 | } 111 | } 112 | ); 113 | } 114 | } 115 | 116 | get state() { 117 | const s = states.get(this); 118 | if ((typeof s === 'object') && s !== null) { 119 | Engine.queue(this); 120 | } 121 | return s; 122 | } 123 | 124 | set state(value) { 125 | states.set(this, value); 126 | Engine.queue(this); 127 | } 128 | } 129 | 130 | function compile(node, engine, prefix, markers) { 131 | const prefixLength = prefix.length; 132 | const compileChilds = Array.from(node.attributes) 133 | .filter(a => a.name.substr(0, prefixLength) === prefix) 134 | .reduce((acc, attr) => { 135 | const id = attr.name.substr(prefixLength) 136 | .replace(/-([a-z])/g, g => g[1].toUpperCase()); 137 | 138 | const marker = markers[id]; 139 | 140 | try { 141 | if (!marker) { 142 | throw new ReferenceError(`marker '${id}' not found.`); 143 | } 144 | marker(engine, node, attr.value); 145 | } catch(e) { 146 | const nodeText = node.outerHTML.match(/^<[^<]+>/i); 147 | e.message = `: ${e.message}\n'${id}' -> ${nodeText}`; 148 | throw e; 149 | } 150 | 151 | if (marker._options && marker._options.breakCompile) { 152 | return false; 153 | } 154 | 155 | return acc; 156 | }, true); 157 | 158 | if (compileChilds && node.children.length) { 159 | Array.from(node.children).forEach( 160 | node => compile(node, engine, prefix, markers) 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Markers/Foreach.js: -------------------------------------------------------------------------------- 1 | import IndexedTemplate from './Template/IndexedTemplate'; 2 | import Expression from '../Expression/Expression'; 3 | 4 | class Controller { 5 | constructor(engine, node, evaluate) { 6 | if (!node.children[0]) { 7 | throw new Error('No children elements.'); 8 | } 9 | 10 | this.engine = engine; 11 | this.node = node; 12 | 13 | const expr = new Expression(engine, evaluate); 14 | let list = expr.get(); 15 | let createFromView = false; 16 | 17 | if(list === undefined) { 18 | list = expr.set([], true); 19 | createFromView = true; 20 | } 21 | 22 | if (!Array.isArray(list)) { 23 | throw new TypeError('Invalid foreach target.'); 24 | } 25 | 26 | if (createFromView) { 27 | this.items = this.createFromView(list); 28 | this.template = this.items[this.items.length -1].clone(); 29 | } else { 30 | this.items = this.createFromList(list); 31 | 32 | if (this.items.length) { 33 | this.template = this.items[this.items.length - 1].clone(); 34 | } else { 35 | this.template = new IndexedTemplate(node.children[0], this.engine); 36 | } 37 | 38 | this.clear(list); 39 | } 40 | 41 | expr.observe(this.render.bind(this), false, true); 42 | } 43 | 44 | createFromView(list) { 45 | const length = this.node.children.length; 46 | 47 | return Array.from(this.node.children).map((child, index) => { 48 | const temp = new IndexedTemplate(child, this.engine); 49 | 50 | list[index] = temp 51 | .setState(list[index], { index, length }) 52 | .getState(); 53 | 54 | return temp; 55 | }); 56 | } 57 | 58 | createFromList(list) { 59 | const length = list.length; 60 | let temp; 61 | 62 | return list.map((item, index) => { 63 | if (this.node.children[index]) { 64 | temp = new IndexedTemplate(this.node.children[index], this.engine) 65 | .setState(list[index], { index, length }); 66 | } else { 67 | temp = temp 68 | .clone() 69 | .setState(list[index], { index, length }) 70 | .append(true); 71 | } 72 | 73 | list[index] = temp.getState(); 74 | 75 | return temp; 76 | }); 77 | } 78 | 79 | clear(list) { 80 | const index = list.length; 81 | const max = this.node.children.length; 82 | for (let i = index; i < max; i++) { 83 | this.node.removeChild(this.node.children[index]); 84 | } 85 | } 86 | 87 | render(list, { type, changelog } = {}) { 88 | if (!Array.isArray(list)) { 89 | throw new TypeError('Invalid foreach target.'); 90 | } 91 | 92 | const length = list.length; 93 | 94 | switch(type) { 95 | case 'modify': 96 | Object.assign(this.items, Object.keys(changelog) 97 | .reduce((items, key) => { 98 | const { type, oldKey, newKey } = changelog[key]; 99 | 100 | if (type === 'delete') { 101 | if (this.items[key]) { 102 | this.items[key].remove(); 103 | } 104 | } else { 105 | const index = parseInt(key, 10); 106 | let temp = this.items[oldKey || key]; 107 | let append = false; 108 | 109 | if (oldKey) { 110 | if (this.items[key]) { 111 | this.items[key].remove(); 112 | } 113 | this.items[oldKey] = null; 114 | } 115 | 116 | if (!temp || (newKey && !oldKey)) { 117 | temp = this.template.clone(); 118 | append = true; 119 | } 120 | 121 | temp.setState(list[key], { index, length }); 122 | 123 | if (append || oldKey) { 124 | temp.append(true, this.node.children[index]); 125 | } 126 | 127 | items[key] = temp; 128 | } 129 | 130 | return items; 131 | }, {}) 132 | ); 133 | 134 | this.items.length = list.length; 135 | 136 | break; 137 | 138 | case 'set': 139 | const itemsLength = this.items.length; 140 | const items = list.map((item, index) => { 141 | let temp; 142 | 143 | if (index < itemsLength) { 144 | temp = this.items[index]; 145 | } else { 146 | temp = this.template.clone().append(true); 147 | } 148 | 149 | temp.setState(item, { index, length }); 150 | 151 | return temp; 152 | }); 153 | 154 | for(let i = length; i < itemsLength; i++) { 155 | this.items[i].remove(); 156 | } 157 | 158 | this.items = items; 159 | } 160 | } 161 | } 162 | 163 | export default function Foreach(engine, node, evaluate) { 164 | return new Controller(engine, node, evaluate); 165 | } 166 | 167 | Foreach._options = { 168 | breakCompile: true 169 | }; 170 | -------------------------------------------------------------------------------- /test/Expression/Expression.js: -------------------------------------------------------------------------------- 1 | import Expression from '../../src/Expression/Expression'; 2 | import Engine from '../../src/Engine'; 3 | 4 | describe('Expression', ()=> { 5 | let expr, engine, filter, spyGet, spySet; 6 | 7 | beforeEach(()=> { 8 | filter = { get: v => v, set: v => v }; 9 | engine = new Engine(document.createElement('div')); 10 | Engine.config(engine).filters.filter = filter; 11 | 12 | spyGet = spyOn(filter, 'get').and.callThrough(); 13 | spySet = spyOn(filter, 'set').and.callThrough(); 14 | }); 15 | 16 | describe('filter', ()=> { 17 | beforeEach(()=> { 18 | 19 | expr = new Expression(engine, 'test|filter'); 20 | }); 21 | 22 | it('call filter get method', ()=> { 23 | expr.get(); 24 | expect(spyGet).toHaveBeenCalled(); 25 | }); 26 | 27 | it('call filter set method', ()=> { 28 | expr.set(1); 29 | expect(spySet).toHaveBeenCalled(); 30 | }); 31 | }); 32 | 33 | describe('`!` flag', ()=> { 34 | beforeEach(()=> { 35 | expr = new Expression(engine, '!test|filter'); 36 | }); 37 | 38 | it('get negative value of target property', ()=> { 39 | engine.state.test = true; 40 | expect(expr.get()).toEqual(false); 41 | }); 42 | 43 | it('set negative value to target property', ()=> { 44 | expr.set(true); 45 | expect(engine.state.test).toEqual(false); 46 | }); 47 | 48 | it('not change get filter', ()=> { 49 | expr.get(); 50 | expect(spyGet).toHaveBeenCalled(); 51 | }); 52 | 53 | it('not change set filter', ()=> { 54 | expr.set(1); 55 | expect(spySet).toHaveBeenCalled(); 56 | }); 57 | }); 58 | 59 | describe('`@` flag', ()=> { 60 | beforeEach(()=> { 61 | expr = new Expression(engine, '@test'); 62 | }); 63 | 64 | it('get from engine target', ()=> { 65 | engine.test = "test"; 66 | expect(expr.get()).toEqual("test"); 67 | }); 68 | 69 | it('set to engine target', ()=> { 70 | expr.set("test"); 71 | expect(engine.test).toEqual("test"); 72 | }); 73 | }); 74 | 75 | describe('`&` flag', ()=> { 76 | beforeEach(()=> { 77 | expr = new Expression(engine, '&test'); 78 | }); 79 | 80 | it('makes target property readonly', ()=> { 81 | engine.state.test = 'default value'; 82 | expr.set('new value'); 83 | expect(expr.get()).toEqual('default value'); 84 | }); 85 | 86 | it('makes set not working', ()=> { 87 | expr.set('new value'); 88 | expect(expr.get()).toBeUndefined(); 89 | expect(engine.state.test).toBeUndefined(); 90 | }); 91 | }); 92 | 93 | describe('`*` flag', ()=> { 94 | let spy; 95 | 96 | beforeEach(()=> { 97 | engine = new Engine(document.createElement('div'), { 98 | state: { test: {} } 99 | }); 100 | spy = jasmine.createSpy('callback'); 101 | expr = new Expression(engine, '*test'); 102 | }); 103 | 104 | it('makes observe deep option default to true', (done)=> { 105 | expr.observe(spy); 106 | engine.state.test.some = 'other'; 107 | 108 | window.requestAnimationFrame(() => { 109 | expect(spy).toHaveBeenCalled(); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('observe', ()=> { 116 | let spy; 117 | 118 | beforeEach(()=> { 119 | engine = new Engine(document.createElement('div')); 120 | expr = new Expression(engine, 'test'); 121 | spy = jasmine.createSpy('callback'); 122 | }); 123 | 124 | it('call when expression changed', (done)=> { 125 | engine.state.test = 'value'; 126 | expr.observe(spy); 127 | 128 | engine.state.test = 'new value'; 129 | 130 | window.requestAnimationFrame(() => { 131 | expect(spy).toHaveBeenCalledWith('new value'); 132 | done(); 133 | }); 134 | }); 135 | 136 | it('call when initialized', (done)=> { 137 | engine.state.test = 'value'; 138 | expr.observe(spy, true); 139 | 140 | window.requestAnimationFrame(() => { 141 | expect(spy).toHaveBeenCalledWith('value'); 142 | done(); 143 | }); 144 | }); 145 | 146 | it('not observe deep properties of object', (done)=> { 147 | engine.state.test = {}; 148 | expr.observe(spy); 149 | engine.state.test.some = 'value'; 150 | 151 | window.requestAnimationFrame(() => { 152 | expect(spy).not.toHaveBeenCalled(); 153 | done(); 154 | }); 155 | }); 156 | 157 | it('observe deep properties of object', (done)=> { 158 | engine.state.test = {}; 159 | expr.observe(spy, false, true); 160 | engine.state.test.some = 'value'; 161 | 162 | window.requestAnimationFrame(() => { 163 | expect(spy).toHaveBeenCalledWith(engine.state.test, { 164 | type: 'modify', changelog: { 'some': { type: 'set' }} 165 | }); 166 | done(); 167 | }); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/Markers/Link.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../src/Engine'; 2 | import Link from '../../src/Markers/Link'; 3 | 4 | describe('Link', ()=> { 5 | let engine, node; 6 | 7 | beforeEach(()=> { 8 | engine = new Engine(document.createElement('div')); 9 | }); 10 | 11 | describe('text input', ()=> { 12 | beforeEach(()=> { 13 | node = document.createElement('input'); 14 | node.type = 'text'; 15 | node.value = 'test'; 16 | }); 17 | 18 | it('set value from view', ()=> { 19 | Link(engine, node, 'test'); 20 | expect(engine.state.test).toEqual('test'); 21 | }); 22 | 23 | it('get value from state', (done)=> { 24 | engine.state.test = 'abc'; 25 | Link(engine, node, 'test'); 26 | 27 | window.requestAnimationFrame(()=> { 28 | expect(node.value).toEqual('abc'); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('push value to state', ()=> { 34 | Link(engine, node, 'test'); 35 | node.value = 'new value'; 36 | const event = document.createEvent('Event'); 37 | event.initEvent('input', true, false); 38 | node.dispatchEvent(event); 39 | 40 | expect(engine.state.test).toEqual('new value'); 41 | }); 42 | }); 43 | 44 | describe('textarea', ()=> { 45 | beforeEach(()=> { 46 | node = document.createElement('textarea'); 47 | node.value = 'test'; 48 | }); 49 | 50 | it('set value from view', ()=> { 51 | Link(engine, node, 'test'); 52 | expect(engine.state.test).toEqual('test'); 53 | }); 54 | 55 | it('get value from state', (done)=> { 56 | engine.state.test = 'abc'; 57 | Link(engine, node, 'test'); 58 | 59 | window.requestAnimationFrame(()=> { 60 | expect(node.value).toEqual('abc'); 61 | done(); 62 | }); 63 | }); 64 | 65 | it('push value to state', ()=> { 66 | Link(engine, node, 'test'); 67 | node.value = 'new value'; 68 | const event = document.createEvent('Event'); 69 | event.initEvent('input', true, false); 70 | node.dispatchEvent(event); 71 | 72 | expect(engine.state.test).toEqual('new value'); 73 | }); 74 | }); 75 | 76 | describe('radio input', ()=> { 77 | beforeEach(()=> { 78 | node = document.createElement('div'); 79 | node.innerHTML = ` 80 | 81 | 82 | `; 83 | }); 84 | 85 | it('set first input checked property', ()=> { 86 | engine.state.test = 'one'; 87 | Link(engine, node.children[0], 'test'); 88 | Link(engine, node.children[1], 'test'); 89 | 90 | expect(node.children[0].checked).toEqual(true); 91 | }); 92 | 93 | it('set second input checked property', ()=> { 94 | engine.state.test = 'two'; 95 | Link(engine, node.children[0], 'test'); 96 | Link(engine, node.children[1], 'test'); 97 | 98 | expect(node.children[1].checked).toEqual(true); 99 | }); 100 | 101 | it('get value from first checked input', ()=> { 102 | node.children[0].checked = true; 103 | 104 | Link(engine, node.children[0], 'test'); 105 | Link(engine, node.children[1], 'test'); 106 | 107 | expect(engine.state.test).toEqual('one'); 108 | }); 109 | 110 | it('get value from first checked input', ()=> { 111 | node.children[1].checked = true; 112 | 113 | Link(engine, node.children[0], 'test'); 114 | Link(engine, node.children[1], 'test'); 115 | 116 | expect(engine.state.test).toEqual('two'); 117 | }); 118 | 119 | it('push value to state', ()=> { 120 | document.body.appendChild(node); 121 | 122 | node.children[0].checked = true; 123 | Link(engine, node.children[0], 'test'); 124 | Link(engine, node.children[1], 'test'); 125 | 126 | node.children[1].checked = true; 127 | 128 | const event = document.createEvent('Event'); 129 | event.initEvent('change', true, false); 130 | 131 | node.children[0].dispatchEvent(event); 132 | node.children[1].dispatchEvent(event); 133 | 134 | expect(engine.state.test).toEqual('two'); 135 | 136 | document.body.removeChild(node); 137 | }); 138 | 139 | }); 140 | 141 | describe('select', ()=> { 142 | beforeEach(()=> { 143 | node = document.createElement('div'); 144 | node.innerHTML = ` 145 | 149 | `; 150 | node = node.children[0]; 151 | node.options[0].selected = true; 152 | }); 153 | 154 | it('set value from view', ()=> { 155 | Link(engine, node, 'test'); 156 | expect(engine.state.test).toEqual('1'); 157 | }); 158 | 159 | it('get value from state', ()=> { 160 | engine.state.test = '2'; 161 | Link(engine, node, 'test'); 162 | expect(node.value).toEqual('2'); 163 | }); 164 | 165 | it('push value to state', ()=> { 166 | Link(engine, node, 'test'); 167 | 168 | node.options[1].selected = true; 169 | const event = document.createEvent('Event'); 170 | event.initEvent('change', true, false); 171 | 172 | node.dispatchEvent(event); 173 | 174 | expect(engine.state.test).toEqual('2'); 175 | }); 176 | 177 | it('refresh select value after options mutation', (done)=> { 178 | Link(engine, node, 'test'); 179 | 180 | node.options[0].setAttribute('value', '2'); 181 | node.options[1].setAttribute('value', '1'); 182 | 183 | window.requestAnimationFrame(()=> { 184 | expect(node.value).toEqual('1'); 185 | done(); 186 | }); 187 | 188 | }); 189 | }); 190 | 191 | describe('select with multiple option', ()=> { 192 | beforeEach(()=> { 193 | node = document.createElement('div'); 194 | node.innerHTML = ` 195 | 201 | `; 202 | node = node.children[0]; 203 | node.options[0].selected = true; 204 | node.options[2].selected = true; 205 | }); 206 | 207 | it('set value from view', ()=> { 208 | Link(engine, node, 'test'); 209 | expect(engine.state.test).toEqual(['1','3']); 210 | }); 211 | 212 | it('get value from state', ()=> { 213 | engine.state.test = ['2','4']; 214 | Link(engine, node, 'test'); 215 | expect(node.options[1].selected).toEqual(true); 216 | expect(node.options[3].selected).toEqual(true); 217 | }); 218 | 219 | it('push value to state', ()=> { 220 | Link(engine, node, 'test'); 221 | 222 | node.options[1].selected = true; 223 | const event = document.createEvent('Event'); 224 | event.initEvent('change', true, false); 225 | 226 | node.dispatchEvent(event); 227 | 228 | expect(engine.state.test).toEqual(['1','2','3']); 229 | }); 230 | 231 | it('update target object in state', ()=> { 232 | Link(engine, node, 'test'); 233 | 234 | const target = engine.state.test; 235 | 236 | node.options[1].selected = true; 237 | const event = document.createEvent('Event'); 238 | event.initEvent('change', true, false); 239 | 240 | node.dispatchEvent(event); 241 | 242 | expect(engine.state.test === target).toEqual(true); 243 | }); 244 | 245 | it('refresh select value after options mutation', (done)=> { 246 | Link(engine, node, 'test'); 247 | 248 | node.options[0].setAttribute('value', '2'); 249 | node.options[1].setAttribute('value', '1'); 250 | 251 | window.requestAnimationFrame(()=> { 252 | expect(node.value).toEqual('1'); 253 | done(); 254 | }); 255 | 256 | }); 257 | }); 258 | 259 | 260 | 261 | }); 262 | -------------------------------------------------------------------------------- /test/Markers/Foreach.js: -------------------------------------------------------------------------------- 1 | import Engine from '../../src/Engine'; 2 | import Foreach from '../../src/Markers/Foreach'; 3 | 4 | describe('Foreach', ()=> { 5 | let engine, node, foreach; 6 | 7 | beforeEach(()=> { 8 | node = document.createElement('ul'); 9 | engine = new Engine(document.createElement('div')); 10 | }); 11 | 12 | it('create array content of primitive values', ()=> { 13 | node.innerHTML = ` 14 |
  • Title1
  • 15 |
  • Title2
  • 16 | `; 17 | Foreach(engine, node, 'test'); 18 | expect(engine.state.test).toEqual(["Title1", "Title2"]); 19 | }); 20 | 21 | it('create array content of objects', ()=> { 22 | node.innerHTML = ` 23 |
  • Title1
  • 24 |
  • Title2
  • 25 | `; 26 | 27 | Foreach(engine, node, 'test'); 28 | expect(engine.state.test).toEqual([ 29 | { test: "Title1" }, 30 | { test: "Title2" } 31 | ]); 32 | }); 33 | 34 | it('removes children for empty array', ()=> { 35 | node.innerHTML = ` 36 |
  • Title1
  • 37 |
  • Title2
  • 38 | `; 39 | engine.state.test = []; 40 | Foreach(engine, node, 'test'); 41 | 42 | expect(node.children.length).toEqual(0); 43 | }); 44 | 45 | it('create array of primitives from constructor', ()=> { 46 | node.innerHTML = ` 47 |
  • Title1
  • 48 | `; 49 | 50 | engine.state.test = [undefined, 'two', 'three']; 51 | 52 | Foreach(engine, node, 'test'); 53 | 54 | expect(node.children[0].textContent).toEqual('Title1'); 55 | expect(node.children[1].textContent).toEqual('two'); 56 | expect(node.children[2].textContent).toEqual('three'); 57 | }); 58 | 59 | it('create array of objects from constructor', ()=> { 60 | node.innerHTML = ` 61 |
  • Title1
  • 62 | `; 63 | 64 | engine.state.test = [undefined, { test: 'two'}, { test: 'three'}]; 65 | 66 | Foreach(engine, node, 'test'); 67 | 68 | expect(node.children[0].textContent).toEqual('Title1'); 69 | expect(node.children[1].textContent).toEqual('two'); 70 | expect(node.children[2].textContent).toEqual('three'); 71 | }); 72 | 73 | describe('when manipulate array', ()=> { 74 | beforeEach(()=> { 75 | node.innerHTML = ` 76 |
  • Title2
  • 77 |
  • Title1
  • 78 |
  • Title3
  • 79 | `; 80 | foreach = Foreach(engine, node, 'test'); 81 | }); 82 | 83 | it('adds new element', (done)=> { 84 | engine.state.test.push('Title3'); 85 | 86 | window.requestAnimationFrame(()=> { 87 | expect(node.children.length).toEqual(4); 88 | done(); 89 | }); 90 | }); 91 | 92 | it('removes element', (done)=> { 93 | engine.state.test.shift(); 94 | 95 | window.requestAnimationFrame(()=> { 96 | expect(node.children.length).toEqual(2); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('updates one value', (done)=> { 102 | engine.state.test[0] = 'new state'; 103 | 104 | window.requestAnimationFrame(()=> { 105 | expect(node.children[0].textContent).toEqual('new state'); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('updates many values', (done)=> { 111 | engine.state.test.sort(); 112 | 113 | window.requestAnimationFrame(()=> { 114 | expect(node.children[0].textContent).toEqual('Title1'); 115 | expect(node.children[1].textContent).toEqual('Title2'); 116 | expect(node.children[2].textContent).toEqual('Title3'); 117 | done(); 118 | }); 119 | }); 120 | 121 | it('relocate elements', (done)=> { 122 | const node0 = node.children[0]; 123 | const node1 = node.children[1]; 124 | const node2 = node.children[2]; 125 | 126 | engine.state.test.sort(); 127 | 128 | window.requestAnimationFrame(()=> { 129 | expect(node.children[0]).toEqual(node1); 130 | expect(node.children[1]).toEqual(node0); 131 | expect(node.children[2]).toEqual(node2); 132 | done(); 133 | }); 134 | }); 135 | 136 | it('for multiply changes', (done) => { 137 | const state = engine.state; 138 | state.test.sort(); 139 | state.test[2] = 'New Title'; 140 | state.test[3] = 'Title3'; 141 | 142 | window.requestAnimationFrame(()=> { 143 | expect(node.children[0].textContent).toEqual('Title1'); 144 | expect(node.children[1].textContent).toEqual('Title2'); 145 | expect(node.children[2].textContent).toEqual('New Title'); 146 | expect(node.children[3].textContent).toEqual('Title3'); 147 | done(); 148 | }); 149 | }); 150 | 151 | it('creates new items collection', (done) => { 152 | const state = engine.state; 153 | state.test.sort(); 154 | state.test[2] = 'New Title'; 155 | state.test[3] = 'Title3'; 156 | 157 | window.requestAnimationFrame(()=> { 158 | const items = foreach.items.map(i => { 159 | return i.nodes[0]; 160 | }); 161 | 162 | expect(foreach.items.length).toEqual(4); 163 | expect(Array.from(node.children)).toEqual(items); 164 | done(); 165 | }); 166 | }); 167 | }); 168 | 169 | describe('when set new array', ()=> { 170 | beforeEach(()=> { 171 | node.innerHTML = ` 172 |
  • Title2
  • 173 |
  • Title1
  • 174 |
  • Title3
  • 175 | `; 176 | foreach = Foreach(engine, node, 'test'); 177 | }); 178 | 179 | it('replace elements', (done)=> { 180 | const newArray = ['one', 'two', 'three']; 181 | engine.state.test = newArray; 182 | 183 | window.requestAnimationFrame(()=> { 184 | let values = Array.from(node.children).map(n => n.textContent); 185 | expect(values).toEqual(newArray); 186 | done(); 187 | }); 188 | }); 189 | 190 | it('add new elements', (done)=> { 191 | const newArray = ['one', 'two', 'three', 'four']; 192 | engine.state.test = newArray; 193 | 194 | window.requestAnimationFrame(()=> { 195 | let values = Array.from(node.children).map(n => n.textContent); 196 | expect(values).toEqual(newArray); 197 | done(); 198 | }); 199 | }); 200 | 201 | it('removes elements', (done)=> { 202 | const newArray = ['one']; 203 | engine.state.test = newArray; 204 | 205 | window.requestAnimationFrame(()=> { 206 | let values = Array.from(node.children).map(n => n.textContent); 207 | expect(values).toEqual(newArray); 208 | done(); 209 | }); 210 | }); 211 | 212 | it('updates items collection', (done)=> { 213 | const newArray = ['one']; 214 | engine.state.test = newArray; 215 | 216 | window.requestAnimationFrame(()=> { 217 | let nodes = foreach.items.map(i => i.nodes[0]); 218 | expect(nodes).toEqual(Array.from(node.children)); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('`index` computed property', ()=> { 225 | beforeEach(()=> { 226 | node.innerHTML = ` 227 |
  • a
  • 228 |
  • b
  • 229 | `; 230 | 231 | foreach = Foreach(engine, node, 'test'); 232 | }); 233 | 234 | it('is set from constructor', (done)=> { 235 | window.requestAnimationFrame(()=> { 236 | const items = Array.from(node.children).map(i => i.title); 237 | expect(items).toEqual(["0", "1"]); 238 | done(); 239 | }); 240 | }); 241 | 242 | it('is changed when updated', (done)=> { 243 | engine.state.test = ['a','b','c']; 244 | 245 | window.requestAnimationFrame(()=> { 246 | const items = Array.from(node.children).map(i => i.title); 247 | expect(items).toEqual(["0", "1", "2"]); 248 | done(); 249 | }); 250 | }); 251 | 252 | it('is changed when relocated', (done)=> { 253 | const state = engine.state; 254 | state.test[0] = 'b'; 255 | state.test[1] = 'a'; 256 | 257 | window.requestAnimationFrame(()=> { 258 | const indexes = Array.from(node.children).map(i => i.title); 259 | expect(indexes).toEqual(["0", "1"]); 260 | done(); 261 | }); 262 | }); 263 | }); 264 | 265 | describe('`length` computed property', ()=> { 266 | beforeEach(()=> { 267 | node.innerHTML = ` 268 |
  • a
  • 269 |
  • b
  • 270 | `; 271 | 272 | foreach = Foreach(engine, node, 'test'); 273 | }); 274 | 275 | it('is set from constructor', (done)=> { 276 | window.requestAnimationFrame(()=> { 277 | const items = Array.from(node.children).map(i => i.title); 278 | expect(items).toEqual(["2", "2"]); 279 | done(); 280 | }); 281 | }); 282 | 283 | it('is changed when updated', (done)=> { 284 | engine.state.test = ['a','b','c']; 285 | 286 | window.requestAnimationFrame(()=> { 287 | const items = Array.from(node.children).map(i => i.title); 288 | expect(items).toEqual(["3", "3", "3"]); 289 | done(); 290 | }); 291 | }); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /dist/linked-html.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.LinkedHtml=t():e.LinkedHtml=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return e[r].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(1);Object.defineProperty(t,"Engine",{enumerable:!0,get:function(){return r["default"]}});var i=n(9);Object.defineProperty(t,"Expression",{enumerable:!0,get:function(){return i["default"]}});var o=n(8);Object.defineProperty(t,"register",{enumerable:!0,get:function(){return o.register}})},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e){return e&&"undefined"!=typeof Symbol&&e.constructor===Symbol?"symbol":typeof e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t,n,r){var i=n.length,o=Array.from(e.attributes).filter(function(e){return e.name.substr(0,i)===n}).reduce(function(n,o){var a=o.name.substr(i).replace(/-([a-z])/g,function(e){return e[1].toUpperCase()}),u=r[a];try{if(!u)throw new ReferenceError("marker '"+a+"' not found.");u(t,e,o.value)}catch(s){var c=e.outerHTML.match(/^<[^<]+>/i);throw s.message=": "+s.message+"\n'"+a+"' -> "+c,s}return u._options&&u._options.breakCompile?!1:n},!0);o&&e.children.length&&Array.from(e.children).forEach(function(e){return a(e,t,n,r)})}var u=function(){function e(e,t){for(var n=0;nthis.lastCheck&&!function(){var r=!1;t.lastCheck=n,t.changelog={},Object.keys(t.cache).forEach(function(e){if(!t.target.hasOwnProperty(e)){var n=t.cache[e];t.changelog[e]={type:"delete",oldValue:n},r=!0}});var i=new s["default"],o={};t.cache=Object.keys(t.target).reduce(function(n,a){var u=t.target[a];if(t.cache.hasOwnProperty(a)?e.is(u,t.cache[a])||(t.changelog[a]=t.changelog[a]||{type:"set"},t.changelog[a].oldValue=t.cache[a],r=!0):(t.changelog[a]={type:"set"},r=!0),e.isObject(u)){var s=new e(u);s.isChanged()&&(r=!0,t.changelog[a]||(t.changelog[a]={type:"modify"},t.changelog[a].changelog=s.changelog))}if(t.changelog[a]){var c=t.keyMap.shift(u,a);c&&c!==a&&t.target[c]!==u&&(t.changelog[a].oldKey=c,t.changelog[c]?t.changelog[c].newKey=a:o[c]=a),o[a]&&(t.changelog[a].newKey=o[a])}return n[a]=u,i.set(u,a),n},{}),t.keyMap=i,r?t.lastChange=t.lastCheck:t.changelog=void 0}(),this.lastChange===n}}]),e}();t["default"]=h},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var r=function(){function e(e,t){for(var n=0;n1&&(t={context:e,property:r}),void(e=e[r])):!0}),delete t.context[t.property]}}]),e}();t["default"]=i},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n={"!":function(e){var t=e.filter;e.filter={get:function(e){return t.get(!e)},set:function(e){return t.set(!e)}}},"@":function(e,t){e.context=function(){return t}},"&":function(e){e.set=function(){}},"*":function(e){e.deep=!0}};t["default"]=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function o(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function a(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function u(e,t,n){var r=void 0;switch(t.type){case"checkbox":case"radio":r=new h(t);break;case"select-one":r=new d(t);break;case"select-multiple":r=new p(t);break;default:r=new l(t)}var i=new f["default"](e,n);i.set(r.get(),!0),i.observe(r.set.bind(r),!0,!1),r.observe(function(){return i.set(r.get())})}var s=function(){function e(e,t){for(var n=0;nr;r++)this.node.removeChild(this.node.children[t])}},{key:"render",value:function(e){var t=this,n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],r=n.type,i=n.changelog;if(!Array.isArray(e))throw new TypeError("Invalid foreach target.");var o=e.length;switch(r){case"modify":Object.assign(this.items,Object.keys(i).reduce(function(n,r){var a=i[r],u=a.type,s=a.oldKey,c=a.newKey;if("delete"===u)t.items[r]&&t.items[r].remove();else{var f=parseInt(r,10),l=t.items[s||r],h=!1;s&&(t.items[r]&&t.items[r].remove(),t.items[s]=null),(!l||c&&!s)&&(l=t.template.clone(),h=!0),l.setState(e[r],{index:f,length:o}),(h||s)&&l.append(!0,t.node.children[f]),n[r]=l}return n},{})),this.items.length=e.length;break;case"set":for(var a=this.items.length,u=e.map(function(e,n){var r=void 0;return r=a>n?t.items[n]:t.template.clone().append(!0),r.setState(e,{index:n,length:o}),r}),s=o;a>s;s++)this.items[s].remove();this.items=u}}}]),e}();o._options={breakCompile:!0}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t); 2 | }function u(e){if(h.has(e))return h.get(e);var t=Object.defineProperties(Object.create(e),{number:{get:function(){return this.index+1}},odd:{get:function(){return this.number%2!==0}},even:{get:function(){return this.number%2===0}},first:{get:function(){return 0===this.index}},last:{get:function(){return this.length===this.number}}});return h.set(e,t),t}var s=function(){function e(e,t){for(var n=0;n