├── template ├── var.after ├── var.before ├── license.before ├── license.after ├── md.after ├── amd.after ├── node.after ├── amd.before ├── copyright ├── node.before └── md.before ├── .gitignore ├── test ├── runtime-style-sheet.css ├── .test.js └── vitamer-mixins.js ├── .travis.yml ├── demo ├── input-mirror.js ├── html-binding.js ├── dbmonster.style.css ├── celsius-to-fahrenheit-input.js ├── dbmonster.html ├── celsius-to-fahrenheit-binding-changed.js ├── fake-tweet.js ├── domclass-vs-knockoutjs │ ├── minifiedHTML.js │ └── index.html ├── random-avatar.js ├── libs │ ├── monitor.js │ ├── memory-stats.js │ └── ENV.js ├── celsius-to-fahrenheit-seppuku.js ├── db-monster.js ├── index.html ├── x-greeter.js └── vitamer-mixins.js ├── .npmignore ├── src ├── ie-lte-9.js ├── Data.js ├── JSONKeys.js ├── dom-class-zero.js ├── examples.js ├── dom-class.js └── Bindings.js ├── utils ├── browserify.sh ├── watchify.sh ├── uglifyjs.sh └── jshint.sh ├── LICENSE.txt ├── package.json ├── testrunner.js ├── index.html ├── Makefile └── README.md /template/var.after: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/var.before: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/license.before: -------------------------------------------------------------------------------- 1 | /*! 2 | -------------------------------------------------------------------------------- /template/license.after: -------------------------------------------------------------------------------- 1 | 2 | */ 3 | -------------------------------------------------------------------------------- /template/md.after: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /template/amd.after: -------------------------------------------------------------------------------- 1 | 2 | return DOMClass; 3 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .idea 4 | -------------------------------------------------------------------------------- /template/node.after: -------------------------------------------------------------------------------- 1 | 2 | module.exports = DOMClass; -------------------------------------------------------------------------------- /template/amd.before: -------------------------------------------------------------------------------- 1 | define(['es-class', 'restyle'], function (Class, restyle) { 2 | -------------------------------------------------------------------------------- /test/runtime-style-sheet.css: -------------------------------------------------------------------------------- 1 | runtime-style-sheet { 2 | margin-left: 6px; 3 | } -------------------------------------------------------------------------------- /template/copyright: -------------------------------------------------------------------------------- 1 | /*! (C) Andrea Giammarchi - @WebReflection - Mit Style License */ 2 | -------------------------------------------------------------------------------- /template/node.before: -------------------------------------------------------------------------------- 1 | var 2 | Class = require('es-class'), 3 | restyle = require('restyle') 4 | ; 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | git: 6 | depth: 1 7 | branches: 8 | only: 9 | - master -------------------------------------------------------------------------------- /demo/input-mirror.js: -------------------------------------------------------------------------------- 1 | var InputMirror = DOMClass({ 2 | 'with': DOMClass.bindings, 3 | template: '
' + 4 | '\n' + 5 | '{{value}}' + 6 | '
' 7 | }); -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .travis.yml 3 | src/* 4 | test/* 5 | template/* 6 | template/license.after 7 | template/license.before 8 | utils/* 9 | node_modules/* 10 | build/*.amd.js 11 | Makefile 12 | index.html 13 | testrunner.js -------------------------------------------------------------------------------- /src/ie-lte-9.js: -------------------------------------------------------------------------------- 1 | /*! IE LTE 9 ONLY *//*@cc_on (function(f){window.setTimeout=f(window.setTimeout);window.setInterval=f(window.setInterval)})(function(f){return function(c,t){var a=[].slice.call(arguments,2);return f(function(){c.apply(this,a)},t)}}); @*/ -------------------------------------------------------------------------------- /utils/browserify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | program () { 4 | local bin="$1" 5 | if [ -d "node_modules/$bin" ]; then 6 | bin="$2" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing $1" 11 | npm install $bin >/dev/null 2>&1 12 | bin="$2" 13 | fi 14 | fi 15 | echo $bin 16 | } 17 | 18 | run () { 19 | local bin="$(program 'browserify' 'node_modules/browserify/bin/cmd.js')" 20 | $bin src/main.js -o build/bundle.max.js -d 21 | } 22 | 23 | run -------------------------------------------------------------------------------- /utils/watchify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | program () { 4 | local bin="$1" 5 | if [ -d "node_modules/$bin" ]; then 6 | bin="$2" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing $1" 11 | npm install $bin >/dev/null 2>&1 12 | bin="$2" 13 | fi 14 | fi 15 | echo $bin 16 | } 17 | 18 | run () { 19 | local bin="$(program 'watchify' 'node_modules/watchify/bin/cmd.js')" 20 | $bin src/main.js -o build/bundle.max.js -v 21 | } 22 | 23 | run 24 | -------------------------------------------------------------------------------- /utils/uglifyjs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | run () { 4 | local bin="uglifyjs" 5 | if [ -d "node_modules/uglify-js" ]; then 6 | bin="node_modules/uglify-js/bin/uglifyjs" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing uglify-js" 11 | npm install "uglify-js@1" >/dev/null 2>&1 12 | bin="node_modules/uglify-js/bin/uglifyjs" 13 | fi 14 | fi 15 | echo "$@" >build/bundle.js 16 | $bin --verbose build/bundle.max.js >>build/bundle.js 17 | 18 | } 19 | 20 | run "$@" -------------------------------------------------------------------------------- /demo/html-binding.js: -------------------------------------------------------------------------------- 1 | var HtmlBinding = DOMClass({ 2 | 'with': DOMClass.bindings, 3 | template: 'some `{{text}}` text boldified: {{{bold(text)}}}. ' + 4 | 'Also some plain html: `{{{html}}}`!', 5 | bindings: { 6 | text: 'plain', 7 | html: 'too damn easy', 8 | bold: function (value) { 9 | return '' + this.escape(value) + ''; 10 | }, 11 | escape: function (value) { 12 | return value.replace(/[&<>'"]/g, function (m) { 13 | return '&#' + m.charCodeAt(0) + ';'; 14 | }); 15 | } 16 | } 17 | }); -------------------------------------------------------------------------------- /template/md.before: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 32 | -------------------------------------------------------------------------------- /demo/dbmonster.style.css: -------------------------------------------------------------------------------- 1 | table { 2 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 3 | font-size: 14px; 4 | line-height: 1.42857143; 5 | color: #333; 6 | background-color: #fff; 7 | } 8 | 9 | #link { 10 | position: fixed; 11 | top: 0; right: 0; 12 | font-size: 12px; 13 | padding: 5px 10px; 14 | background: rgba(255,255,255,0.85); 15 | z-index: 5; 16 | box-shadow: 0 0 8px rgba(0,0,0,0.6); 17 | } 18 | #link .center { 19 | display: block; 20 | text-align: center; 21 | } 22 | 23 | .Query { 24 | position: relative; 25 | } 26 | 27 | .Query:hover .popover { 28 | left: -100%; 29 | width: 100%; 30 | display: block; 31 | } -------------------------------------------------------------------------------- /src/Data.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(DOMClass, 'data', { 2 | enumerable: true, 3 | value: { 4 | 5 | /*! (C) 2015 Andrea Giammarchi - Mit Style License */ 6 | 7 | data: function data(key, value) {'use strict'; 8 | /* jshint eqnull:true */ 9 | var v, k = 'data-dom-class-' + String(key).replace( 10 | /([a-z])([A-Z])/g, 11 | function (m, l, U) { 12 | return l + '-' + U.toLowerCase(); 13 | } 14 | ).toLowerCase(); 15 | if (arguments.length === 2) { 16 | if (value == null) { 17 | this.removeAttribute(k); 18 | } else { 19 | this.setAttribute(k, JSON.stringify(value)); 20 | } 21 | } else { 22 | v = this.getAttribute(k); 23 | return v == null ? v : JSON.parse(v); 24 | } 25 | } 26 | } 27 | }); -------------------------------------------------------------------------------- /utils/jshint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | program () { 4 | local bin="$1" 5 | if [ -d "node_modules/$bin" ]; then 6 | bin="$2" 7 | else 8 | if [ "$(which $bin)" = "" ]; then 9 | mkdir -p node_modules 10 | echo "installing $1" 11 | npm install $bin >/dev/null 2>&1 12 | bin="$2" 13 | fi 14 | fi 15 | echo $bin 16 | } 17 | 18 | run () { 19 | local bin="$(program 'jshint' 'node_modules/jshint/bin/jshint')" 20 | local folder=$1 21 | local js="" 22 | # drop some info in the stdout 23 | echo "linting $f ... " 24 | for f in $folder/*; do 25 | # if it's a fodler, go for recursion 26 | if [ -d "$f" ]; then 27 | run "$f" 28 | else 29 | # grab .js files only 30 | js=$(echo "$f" | sed 's/.js//') 31 | if [ "$js" != "$f" ]; then 32 | # finally use jshint to verify the file 33 | $bin "$f" 34 | # in case there was an error 35 | if [[ $? -ne 0 ]] ; then 36 | exit 1 37 | fi 38 | fi 39 | fi 40 | done 41 | } 42 | 43 | run src -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 by WebReflection 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/JSONKeys.js: -------------------------------------------------------------------------------- 1 | // Mixin Example - automatically serialized bindings 2 | var JSONKeys = (function () { 3 | /*! (C) 2015 Andrea Giammarchi - Mit Style License */ 4 | function bind(el, property) { 5 | Object.defineProperty(el, property, { 6 | configurable: true, 7 | get: function () { 8 | return JSON.parse(this.getAttribute(property)); 9 | }, 10 | set: function (value) { 11 | if (value == null) { 12 | this.removeAttribute(property); 13 | } else { 14 | this.setAttribute(property, JSON.stringify(value)); 15 | } 16 | } 17 | }); 18 | } 19 | return { 20 | setJSONKeys: function () { 21 | for (var i = arguments.length; i--; bind(this, arguments[i])); 22 | return this; 23 | } 24 | }; 25 | }()); 26 | 27 | 28 | 29 | // test it for real 30 | var JSONStorage = new DOMClass({ 31 | name: 'json-storage', 32 | with: JSONKeys 33 | }); 34 | 35 | var js = document.body.appendChild(new JSONStorage); 36 | js.setJSONKeys('name', 'age', 'info'); 37 | 38 | js.name = 'Andrea'; 39 | js.age = 37; 40 | js.info = { 41 | currentCity: 'London', 42 | sports: ['Snowboarding', 'Skydiving'] 43 | }; -------------------------------------------------------------------------------- /test/.test.js: -------------------------------------------------------------------------------- 1 | var 2 | fs = require('fs'), 3 | path = require('path'), 4 | spawn = require('child_process').spawn, 5 | modules = path.join(__dirname, '..', 'node_modules', 'wru', 'node', 'program.js'), 6 | tests = [], 7 | ext = /\.js$/, 8 | code = 0, 9 | many = 0; 10 | 11 | function exit($code) { 12 | if ($code) { 13 | code = $code; 14 | } 15 | if (!--many) { 16 | if (!code) { 17 | fs.writeFileSync( 18 | path.join(__dirname, '..', 'index.html'), 19 | fs.readFileSync( 20 | path.join(__dirname, '..', 'index.html'), 21 | 'utf-8' 22 | ).replace(/var TESTS = \[.*?\];/, 'var TESTS = ' + JSON.stringify(tests) + ';'), 23 | 'utf-8' 24 | ); 25 | } 26 | process.exit(code); 27 | } 28 | } 29 | 30 | fs.readdirSync(__dirname).filter(function(file){ 31 | if (ext.test(file) && (fs.existsSync || path.existsSync)(path.join(__dirname, '..', 'src', file))) { 32 | ++many; 33 | tests.push(file.replace(ext, '')); 34 | spawn( 35 | 'node', [modules, path.join('test', file)], { 36 | detached: false, 37 | stdio: [process.stdin, process.stdout, process.stderr] 38 | }).on('exit', exit); 39 | } 40 | }); -------------------------------------------------------------------------------- /demo/celsius-to-fahrenheit-input.js: -------------------------------------------------------------------------------- 1 | var CelsiusToFahrenheitInput = DOMClass({ 2 | 'with': [DOMClass.bindings], 3 | template: '
' + 4 | '°C' + 5 | '' + 6 | '°F' + 7 | '
', 8 | bindings: {}, 9 | constructor: function () { 10 | // whenever an input event bubbles up 11 | this.addEventListener('input', this); 12 | }, 13 | handleEvent: function (e) { 14 | // if it was a celsius, update fahrenheit and vice-versa 15 | var 16 | key = e.target.name, 17 | which = key === 'celsius' ? 18 | 'fahrenheit' : 'celsius', 19 | convert = key === 'celsius' ? 20 | 'toFahrenheit' : 'toCelsius' 21 | ; 22 | this.bindings[which] = this[convert](e.target.value); 23 | }, 24 | setCelsius: function (value) { 25 | this.bindings.celsius = value; 26 | this.bindings.fahrenheit = this.toFahrenheit(value); 27 | }, 28 | setFahrenheit: function (value) { 29 | this.bindings.fahrenheit = value; 30 | this.bindings.celsius = this.toCelsius(value); 31 | }, 32 | toCelsius: function (value) { 33 | return (5/9 * (parseFloat(value) - 32)).toFixed(1); 34 | }, 35 | toFahrenheit: function (value) { 36 | return (9/5 * parseFloat(value) + 32).toFixed(1); 37 | } 38 | }); -------------------------------------------------------------------------------- /demo/dbmonster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DBMonster via DOMClass & bindings Mixin 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /demo/celsius-to-fahrenheit-binding-changed.js: -------------------------------------------------------------------------------- 1 | var CelsiusToFahrenheitBindingChanged = DOMClass({ 2 | 'with': [DOMClass.bindings], 3 | template: '
' + 4 | '°C' + 5 | '' + 6 | '°F' + 7 | '
', 8 | bindings: {}, 9 | dispatchBindings: true, 10 | constructor: function () { 11 | this.addEventListener('bindingChanged', this); 12 | }, 13 | handleEvent: function (e) { 14 | // if it was a celsius, update fahrenheit and vice-versa 15 | var 16 | key = e.detail.key, 17 | which = key === 'celsius' ? 18 | 'fahrenheit' : 'celsius', 19 | convert = key === 'celsius' ? 20 | 'toFahrenheit' : 'toCelsius', 21 | value = this[convert](e.detail.value) 22 | ; 23 | // avoid repeated dispatch when value is the same 24 | if (value !== this.bindings[which]) 25 | this.bindings[which] = value; 26 | }, 27 | toCelsius: function (value) { 28 | return (5/9 * (parseFloat(value) - 32)).toFixed(1); 29 | }, 30 | toFahrenheit: function (value) { 31 | return (9/5 * parseFloat(value) + 32).toFixed(1); 32 | }, 33 | setCelsius: function (value) { 34 | // this will cause a dispatch 35 | this.bindings.celsius = value; 36 | }, 37 | setFahrenheit: function (value) { 38 | // this will cause a dispatch 39 | this.bindings.fahrenheit = value; 40 | } 41 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.7.0", 3 | "license": "MIT", 4 | "name": "dom-class", 5 | "description": "A lightweight, cross browser, simplification of WebComponents", 6 | "homepage": "https://github.com/WebReflection/dom-class", 7 | "keywords": [ 8 | "custom", 9 | "elements", 10 | "class", 11 | "vitamer", 12 | "dom", 13 | "restyle", 14 | "es-class" 15 | ], 16 | "author": { 17 | "name": "Andrea Giammarchi", 18 | "web": "http://webreflection.blogspot.com/" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/WebReflection/dom-class.git" 23 | }, 24 | "main": "./build/dom-class.node.js", 25 | "scripts": { 26 | "test": "phantomjs testrunner.js", 27 | "web": "node node_modules/tiny-cdn/tiny-cdn run -p=1337", 28 | "demo": "node node_modules/tiny-cdn/tiny-cdn run -p=1337 -s=demo -d=demo", 29 | "install-global-dev": "node -e '(function(o){for(var k in o)require(\"child_process\").spawn(\"npm\",[\"install\",\"-g\",k+\"@\"+o[k]]).on(\"exit\",console.log.bind(console,k+\"@\"+o[k]));}(require(\"package.json\").globalDevDependencies))'" 30 | }, 31 | "globalDevDependencies": { 32 | "uglify-js": "1", 33 | "jshint": "2", 34 | "browserify": "*", 35 | "watchify": "*", 36 | "phantomjs-prebuilt": "*", 37 | "tiny-cdn": "*" 38 | }, 39 | "devDependencies": { 40 | "wru": "*", 41 | "document-register-element": "*", 42 | "dom4": "*", 43 | "query-result": "*" 44 | }, 45 | "dependencies": { 46 | "es-class": "*", 47 | "restyle": "*" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /demo/fake-tweet.js: -------------------------------------------------------------------------------- 1 | var FakeTweet = DOMClass({ 2 | 'with': DOMClass.bindings, 3 | css: { 4 | '': { 5 | display: 'block', 6 | position: 'relative' 7 | }, 8 | '.left': { 9 | width: 64, 10 | position: 'absolute' 11 | }, 12 | '.right': { 13 | padding: '8px 8px 8px 72px' 14 | }, 15 | 'header .handler, header .time': { 16 | marginLeft: 8, 17 | fontSize: '.8em', 18 | color: '#666' 19 | }, 20 | 'header .time:before': { 21 | content: '"-"', 22 | marginRight: 8 23 | }, 24 | 'footer a, footer a:hover': { 25 | textDecoration: 'none', 26 | color: '#666', 27 | fontSize: 'bold', 28 | marginRight: '15%' 29 | }, 30 | 'footer a:first-child': { 31 | marginLeft: '15%' 32 | }, 33 | 'strong, .content': { 34 | fontSize: '.9em' 35 | } 36 | }, 37 | template: ''.concat( 38 | '
', 39 | '', 40 | '
', 41 | '
', 42 | '
', 43 | '{{name}}', 44 | '{{handler}}', 45 | '{{time}}', 46 | '
', 47 | '
{{content}}
', 48 | '', 54 | '
' 55 | ), 56 | constructor: function (info) { 57 | for (var key in info) this.bindings[key] = info[key]; 58 | } 59 | }); -------------------------------------------------------------------------------- /demo/domclass-vs-knockoutjs/minifiedHTML.js: -------------------------------------------------------------------------------- 1 | var minifiedHTML = (function (escaped, es, trim, voidType) { 2 | function attributes(el) { 3 | for (var 4 | attr, 5 | value, 6 | out = [], 7 | attrs = el.attributes, 8 | len = attrs.length, 9 | i = 0; i < len; i++ 10 | ) { 11 | attr = attrs[i]; 12 | value = attr.value || attr.nodeValue || ''; 13 | out.push(' ', attr.name || attr.nodeName); 14 | if (value) out.push('="', value.replace(es, cape), '"'); 15 | } 16 | return out.join(''); 17 | } 18 | function cape(m) { 19 | return escaped[m]; 20 | } 21 | function minifiedHTML(el) { 22 | if (el.nodeType !== 1) return ''; 23 | for (var 24 | child, 25 | nodeName = (el.nodeName || el.tagName).toLowerCase(), 26 | out = ['<', nodeName, attributes(el), '>'], 27 | childNodes = el.childNodes, 28 | len = childNodes.length, 29 | i = 0; i < len; i++ 30 | ) { 31 | child = childNodes[i]; 32 | switch (child.nodeType) { 33 | case 1: out.push(minifiedHTML(child)); 34 | break; 35 | case 3: out.push(child.textContent.replace(trim, '').replace(es, cape)); 36 | break; 37 | } 38 | } 39 | if (!voidType.test(nodeName)) out.push(''); 40 | return out.join(''); 41 | } 42 | return minifiedHTML; 43 | }( 44 | { 45 | '&': '&', 46 | '<': '<', 47 | '>': '>', 48 | "'": ''', 49 | '"': '"' 50 | }, 51 | /[&<>'"]/g, 52 | /^\s+|\s+$/g, 53 | /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i 54 | )); -------------------------------------------------------------------------------- /demo/random-avatar.js: -------------------------------------------------------------------------------- 1 | var RandomAvatar = DOMClass({ 2 | name: 'random-avatar', 3 | constructor: function (width) { 4 | this.textContent = ''; 5 | var 6 | canvas = (this.ownerDocument || document).createElement('canvas'), 7 | context = canvas.getContext('2d'), 8 | pr = window.devicePixelRatio || 1, 9 | size = width || parseFloat(this.getAttribute('size')), 10 | diameter = size * pr, 11 | radius = diameter / 2, 12 | lineWidth = (size * .06) * pr, 13 | random = Math.random 14 | ; 15 | canvas.width = canvas.height = diameter; 16 | canvas.style.cssText = ''.concat( 17 | 'width:', size, 'px;', 18 | 'height:', size, 'px;' 19 | ); 20 | context.beginPath(); 21 | context.arc(radius, radius, radius - lineWidth, 0, 2 * Math.PI, false); 22 | // background 23 | context.fillStyle = 'rgb('.concat( 24 | [ 25 | random() * 256 | 0, 26 | random() * 256 | 0, 27 | random() * 256 | 0 28 | ].join(','), 29 | ')' 30 | ); 31 | context.fill(); 32 | // border 33 | context.lineWidth = lineWidth; 34 | context.strokeStyle = 'rgb('.concat( 35 | [ 36 | random() * 256 | 0, 37 | random() * 256 | 0, 38 | random() * 256 | 0 39 | ].join(','), 40 | ')' 41 | ); 42 | context.stroke(); 43 | // text 44 | context.fillStyle = context.strokeStyle; 45 | context.font = (radius | 0) + 'px sans-serif'; 46 | context.textAlign = 'center'; 47 | context.fillText( 48 | String.fromCharCode(48 + (random() * 75 | 0)), 49 | radius, 50 | radius + radius / 3 51 | ); 52 | context.closePath(); 53 | if (canvas.toDataURL) { 54 | this.appendChild(new Image(size, size)).src = canvas.toDataURL(); 55 | } else { 56 | this.append(canvas); 57 | } 58 | } 59 | }); -------------------------------------------------------------------------------- /demo/libs/monitor.js: -------------------------------------------------------------------------------- 1 | var Monitoring = Monitoring || (function() { 2 | 3 | var stats = new MemoryStats(); 4 | stats.domElement.style.position = 'fixed'; 5 | stats.domElement.style.right = '0px'; 6 | stats.domElement.style.bottom = '0px'; 7 | document.body.appendChild( stats.domElement ); 8 | requestAnimationFrame(function rAFloop(){ 9 | stats.update(); 10 | requestAnimationFrame(rAFloop); 11 | }); 12 | 13 | var RenderRate = function () { 14 | var container = document.createElement( 'div' ); 15 | container.id = 'stats'; 16 | container.style.cssText = 'width:150px;opacity:0.9;cursor:pointer;position:fixed;right:80px;bottom:0px;'; 17 | 18 | var msDiv = document.createElement( 'div' ); 19 | msDiv.id = 'ms'; 20 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;'; 21 | container.appendChild( msDiv ); 22 | 23 | var msText = document.createElement( 'div' ); 24 | msText.id = 'msText'; 25 | msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 26 | msText.innerHTML= 'Repaint rate: 0/sec'; 27 | msDiv.appendChild( msText ); 28 | 29 | var bucketSize = 20; 30 | var bucket = []; 31 | var lastTime = Date.now(); 32 | return { 33 | domElement: container, 34 | ping: function () { 35 | var start = lastTime; 36 | var stop = Date.now(); 37 | var rate = 1000 / (stop - start); 38 | bucket.push(rate); 39 | if (bucket.length > bucketSize) { 40 | bucket.shift(); 41 | } 42 | var sum = 0; 43 | for (var i = 0; i < bucket.length; i++) { 44 | sum = sum + bucket[i]; 45 | } 46 | msText.textContent = "Repaint rate: " + (sum / bucket.length).toFixed(2) + "/sec"; 47 | lastTime = stop; 48 | } 49 | } 50 | }; 51 | 52 | var renderRate = new RenderRate(); 53 | document.body.appendChild( renderRate.domElement ); 54 | 55 | return { 56 | memoryStats: stats, 57 | renderRate: renderRate 58 | }; 59 | 60 | })(); -------------------------------------------------------------------------------- /testrunner.js: -------------------------------------------------------------------------------- 1 | console.log('Loading: test.html'); 2 | var page = require('webpage').create(); 3 | var url = 'index.html'; 4 | page.onInitialized = function () { 5 | page.evaluate(function () { 6 | window.__PHANTOMJS__ = true; 7 | }); 8 | }; 9 | page.open(url, function (status) { 10 | if (status === 'success') { 11 | setTimeout(function () { 12 | var results = page.evaluate(function() { 13 | // remove the first node with the total from the following counts 14 | var passed = Math.max(0, document.querySelectorAll('.pass').length - 1); 15 | // return {total: document.body.innerHTML}; 16 | return { 17 | // retrieve the total executed tests number 18 | total: ''.concat( 19 | passed, 20 | ' blocks (', 21 | document.querySelector('#wru strong').textContent.replace(/\D/g, ''), 22 | ' single tests)' 23 | ), 24 | passed: passed, 25 | failed: Math.max(0, document.querySelectorAll('.fail').length - 1), 26 | failures: [].map.call(document.querySelectorAll('.fail'), function (node) { 27 | return node.textContent; 28 | }), 29 | errored: Math.max(0, document.querySelectorAll('.error').length - 1), 30 | errors: [].map.call(document.querySelectorAll('.error'), function (node) { 31 | return node.textContent; 32 | }) 33 | }; 34 | }); 35 | console.log('- - - - - - - - - -'); 36 | console.log('total: ' + results.total); 37 | console.log('- - - - - - - - - -'); 38 | console.log('passed: ' + results.passed); 39 | if (results.failed) { 40 | console.log('failures: \n' + results.failures.join('\n')); 41 | } else { 42 | console.log('failed: ' + results.failed); 43 | } 44 | if (results.errored) { 45 | console.log('errors: \n' + results.errors.join('\n')); 46 | } else { 47 | console.log('errored: ' + results.errored); 48 | } 49 | console.log('- - - - - - - - - -'); 50 | if (0 < results.failed + results.errored) { 51 | status = 'failed'; 52 | } 53 | phantom.exit(0); 54 | }, 3000); 55 | } else { 56 | phantom.exit(1); 57 | } 58 | }); -------------------------------------------------------------------------------- /demo/celsius-to-fahrenheit-seppuku.js: -------------------------------------------------------------------------------- 1 | var CelsiusToFahrenheitSeppuku = DOMClass({ 2 | 'with': [DOMClass.bindings], 3 | template: '
' + 4 | '°C' + 5 | '' + 6 | '°F' + 7 | '
', 8 | bindings: { 9 | // bindings can be setters too but it's easy to create a call-stack mess 10 | // this is just an example on how to instantly update two fields 11 | // no matters how the property is changed (user, ce.bindings, input.value) 12 | set celsius(value) { this.update('celsius', 'fahrenheit', this.toFahrenheit, value); }, 13 | set fahrenheit(value) { this.update('fahrenheit', 'celsius', this.toCelsius, value); }, 14 | // the main catch is to avoid recursive callbacks 15 | update: function (binding, change, converter, value) { 16 | // we need to ignore redundant calls 17 | if (!(binding in this.state)) { 18 | this.state[binding] = true; 19 | // we should also avoid changes in the other target 20 | if (!(change in this.state)) { 21 | // at this point we need to trigger the UI change 22 | // this is quite dirty approach because we deal with 23 | // UI in an entire DATA related context (data bindings) 24 | // However, we locked bindings out of their same setters 25 | // so this is actually kinda safe (and will try to invoke setters again) 26 | this[change + 'View'].value = converter.call(this, value); 27 | } 28 | delete this.state[binding]; 29 | } 30 | }, 31 | toCelsius: function (value) { 32 | return (5/9 * (parseFloat(value) - 32)).toFixed(1); 33 | }, 34 | toFahrenheit: function (value) { 35 | return (9/5 * parseFloat(value) + 32).toFixed(1); 36 | } 37 | }, 38 | constructor: function () { 39 | // enrich current bindings with a state 40 | // and give it the ability to directly change the UI 41 | this.bindings.state = Object.create(null); 42 | this.bindings.celsiusView = this.query('[name="celsius"]'); 43 | this.bindings.fahrenheitView = this.query('[name="fahrenheit"]'); 44 | } 45 | }); -------------------------------------------------------------------------------- /src/dom-class-zero.js: -------------------------------------------------------------------------------- 1 | var DOMClass = (function (O,o) { 2 | 3 | /*! (C) Andrea Giammarchi */ 4 | 5 | var 6 | create = O.create, 7 | css = create(null), 8 | dP = O.defineProperty, 9 | gOPD = O.getOwnPropertyDescriptor, 10 | gOPN = O.getOwnPropertyNames, 11 | gOPS = O.getOwnPropertySymbols, 12 | ownKeys = gOPS ? 13 | function (object) { 14 | return gOPN(object).concat(gOPS(object)); 15 | } : 16 | gOPN, 17 | loadCSS = function (document, href) { 18 | var 19 | head = document.head, 20 | link = document.createElement('link') 21 | ; 22 | link.rel = 'stylesheet'; 23 | link.type = 'text/css'; 24 | link.href = href; 25 | css[href] = head.insertBefore(link, head.lastChild); 26 | } 27 | ; 28 | 29 | return function DOMClass(description) { 30 | for (var 31 | k, name, xtends, 32 | constructor, 33 | stylesheet, 34 | descriptors = {}, 35 | keys = ownKeys(description), 36 | set = function (s) { 37 | dP(descriptors, s, { 38 | enumerable: true, 39 | writable: true, 40 | value: gOPD(description, k) 41 | }); 42 | }, 43 | i = 0; i < keys.length; i++ 44 | ) { 45 | k = keys[i]; 46 | switch (k.toLowerCase()) { 47 | case 'name': name = description[k]; break; 48 | case 'stylesheet': stylesheet = description[k]; break; 49 | case 'extends': 50 | xtends = typeof description[k] === 'function' ? 51 | description[k].prototype : description[k]; 52 | break; 53 | case 'constructor': constructor = description[k]; 54 | set('createdCallback'); break; 55 | case 'onattached': set('attachedCallback'); break; 56 | case 'onchanged': set('attributeChangedCallback'); break; 57 | case 'ondetached': set('detachedCallback'); break; 58 | default: set(k); break; 59 | } 60 | } 61 | if (stylesheet) { 62 | descriptors.createdCallback.value = function () { 63 | if (!(stylesheet in css)) 64 | loadCSS(this.ownerDocument || document, stylesheet); 65 | if (constructor) constructor.apply(this, arguments); 66 | }; 67 | } 68 | return document.registerElement( 69 | name || ('zero-dom-class-'+ ++o), 70 | {prototype: create(xtends || HTMLElement.prototype, descriptors)} 71 | ); 72 | }; 73 | 74 | }(Object, 0)); -------------------------------------------------------------------------------- /demo/libs/memory-stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | * @author jetienne / http://jetienne.com/ 4 | * @author paulirish / http://paulirish.com/ 5 | */ 6 | var MemoryStats = function (){ 7 | 8 | var msMin = 100; 9 | var msMax = 0; 10 | 11 | var container = document.createElement( 'div' ); 12 | container.id = 'stats'; 13 | container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer'; 14 | 15 | var msDiv = document.createElement( 'div' ); 16 | msDiv.id = 'ms'; 17 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;'; 18 | container.appendChild( msDiv ); 19 | 20 | var msText = document.createElement( 'div' ); 21 | msText.id = 'msText'; 22 | msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 23 | msText.innerHTML= 'Memory'; 24 | msDiv.appendChild( msText ); 25 | 26 | var msGraph = document.createElement( 'div' ); 27 | msGraph.id = 'msGraph'; 28 | msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0'; 29 | msDiv.appendChild( msGraph ); 30 | 31 | while ( msGraph.children.length < 74 ) { 32 | 33 | var bar = document.createElement( 'span' ); 34 | bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131'; 35 | msGraph.appendChild( bar ); 36 | 37 | } 38 | 39 | var updateGraph = function ( dom, height, color ) { 40 | 41 | var child = dom.appendChild( dom.firstChild ); 42 | child.style.height = height + 'px'; 43 | if( color ) child.style.backgroundColor = color; 44 | 45 | } 46 | 47 | var perf = window.performance || {}; 48 | // polyfill usedJSHeapSize 49 | if (!perf && !perf.memory){ 50 | perf.memory = { usedJSHeapSize : 0 }; 51 | } 52 | if (perf && !perf.memory){ 53 | perf.memory = { usedJSHeapSize : 0 }; 54 | } 55 | 56 | // support of the API? 57 | if( perf.memory.totalJSHeapSize === 0 ){ 58 | console.warn('totalJSHeapSize === 0... performance.memory is only available in Chrome .') 59 | } 60 | 61 | // TODO, add a sanity check to see if values are bucketed. 62 | // If so, reminde user to adopt the --enable-precise-memory-info flag. 63 | // open -a "/Applications/Google Chrome.app" --args --enable-precise-memory-info 64 | 65 | var lastTime = Date.now(); 66 | var lastUsedHeap= perf.memory.usedJSHeapSize; 67 | return { 68 | domElement: container, 69 | 70 | update: function () { 71 | 72 | // refresh only 30time per second 73 | if( Date.now() - lastTime < 1000/30 ) return; 74 | lastTime = Date.now() 75 | 76 | var delta = perf.memory.usedJSHeapSize - lastUsedHeap; 77 | lastUsedHeap = perf.memory.usedJSHeapSize; 78 | var color = delta < 0 ? '#830' : '#131'; 79 | 80 | var ms = perf.memory.usedJSHeapSize; 81 | msMin = Math.min( msMin, ms ); 82 | msMax = Math.max( msMax, ms ); 83 | msText.textContent = "Mem: " + bytesToSize(ms, 2); 84 | 85 | var normValue = ms / (30*1024*1024); 86 | var height = Math.min( 30, 30 - normValue * 30 ); 87 | updateGraph( msGraph, height, color); 88 | 89 | function bytesToSize( bytes, nFractDigit ){ 90 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 91 | if (bytes == 0) return 'n/a'; 92 | nFractDigit = nFractDigit !== undefined ? nFractDigit : 0; 93 | var precision = Math.pow(10, nFractDigit); 94 | var i = Math.floor(Math.log(bytes) / Math.log(1024)); 95 | return Math.round(bytes*precision / Math.pow(1024, i))/precision + ' ' + sizes[i]; 96 | }; 97 | } 98 | 99 | } 100 | 101 | }; -------------------------------------------------------------------------------- /demo/db-monster.js: -------------------------------------------------------------------------------- 1 | var DBMonster = DOMClass({ 2 | name: 'dm-monster', 3 | 'extends': HTMLTableElement, 4 | 'with': DOMClass.bindings, 5 | 'static': { 6 | // each Custom Element row of the bench (200 Row components per benchmark) 7 | // it binds 2 properties and 1 attribute resolved via callback 8 | // it binds 2 properties and 1 invoke per each query result 9 | // total of 12 bindings (properties) with 2 bound call (resolved via methods) 10 | Row: DOMClass({ 11 | name: 'dm-monster-row', 12 | 'extends': HTMLTableRowElement, 13 | 'with': DOMClass.bindings, 14 | constructor: function (db) { 15 | this.createBindings({ 16 | template: ''.concat( 17 | '{{name}}', 18 | '', 19 | '{{queries}}', 20 | '' 21 | ), 22 | bindings: { 23 | name: db.dbname, 24 | queries: db.lastSample.nbQueries, 25 | countClassName: db.lastSample.countClassName 26 | } 27 | }); 28 | this.queryResults = db.lastSample.topFiveQueries.map(function (query) { 29 | return this.appendChild(new DBMonster.QueryResult(query)); 30 | }, this); 31 | }, 32 | update: function (db) { 33 | this.bindings.name = db.dbname; 34 | this.bindings.queries = db.lastSample.nbQueries; 35 | this.bindings.countClassName = db.lastSample.countClassName; 36 | this.queryResults.forEach(this.subUpdate, db.lastSample.topFiveQueries); 37 | }, 38 | subUpdate: function (qr, i) { 39 | qr.update(this[i]); 40 | } 41 | }), 42 | // last 5 Custom Elements per each Row (1000 QueryResult components per benchmark) 43 | // each TD binds 1 property and 1 invoke through the elapsed property 44 | QueryResult: DOMClass({ 45 | name: 'dm-monster-query-cell', 46 | 'extends': HTMLTableCellElement, 47 | 'with': DOMClass.bindings, 48 | constructor: function (query) { 49 | this.createBindings({ 50 | template: ''.concat( 51 | '{{formatElapsed}}', 52 | '
', 53 | '
{{query}}
', 54 | '
', 55 | '
' 56 | ), 57 | bindings: { 58 | query: query.query, 59 | formatElapsed: query.formatElapsed 60 | } 61 | }); 62 | this.className = query.elapsedClassName; 63 | }, 64 | update: function (query) { 65 | this.bindings.query = query.query; 66 | this.bindings.formatElapsed = query.formatElapsed; 67 | this.className = query.elapsedClassName; 68 | } 69 | }) 70 | }, 71 | // the DBMonster Custom Element benchmark. 72 | // total amount of benchmark Custom Elements per instance: 1201 73 | // total amount of benchmark bindings per instance: 2400 74 | // total amount of bound invokes per instance: 1000 75 | // will it perform? Hell YEAH! 76 | // http://webreflection.github.io/dom-class/demo/dbmonster.html 77 | constructor: function (dbs) { 78 | this.className = 'table table-striped latest-data'; 79 | dbs.forEach(function (db) { 80 | this.appendChild(new DBMonster.Row(db)); 81 | }, this.appendChild(document.createElement('tbody'))); 82 | }, 83 | update: function (dbs) { 84 | Array.prototype.forEach.call( 85 | this.firstChild.children, this.subUpdate, dbs); 86 | }, 87 | subUpdate: function (row, i) { 88 | row.update(this[i]); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vitamer Mixins Demo 5 | 6 | 7 | 8 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/examples.js: -------------------------------------------------------------------------------- 1 | 2 | // examples, feel free to ignore this file, is rather pointless in its current state 3 | 4 | 5 | /* 6 | 7 | // test it for real 8 | var EDiv = new DOMClass({ 9 | with: Data, 10 | name: 'e-div', 11 | css: {'': {display: 'block'}}, 12 | onChanged: function () { 13 | console.log(arguments); 14 | } 15 | }); 16 | 17 | var ediv = document.body.appendChild(new EDiv); 18 | 19 | ediv.data('name', 'Andrea'); 20 | ediv.data('age', 37); 21 | ediv.data('info', { 22 | currentCity: 'London', 23 | sports: ['Snowboarding', 'Skydiving'] 24 | }); 25 | 26 | //*/ 27 | 28 | /* 29 | document.body.innerHTML = ''; 30 | Bindings.withBindings.call(document.body, { 31 | template: '' + 32 | '' + 33 | '' + 34 | '
' + 35 | 'The sum of {{a}} + {{b}} is equal to {{sum(a,b)}}', 36 | bindings: {a: 0, b: 0, sum: function (a, b) { 37 | var result = parseFloat(a) + parseFloat(b); 38 | return isNaN(result) ? '?' : result; 39 | }} 40 | }); 41 | //*/ 42 | 43 | /* 44 | 45 | document.body.innerHTML = ''; 46 | Bindings.withBindings.call(document.body, { 47 | template: '{{sum(a,b)}} or just {{a}} and just {{b}}', 48 | bindings: {a: 1, b: 2, sum: function (a, b) { 49 | return a + b; 50 | }} 51 | }); 52 | 53 | */ 54 | 55 | /* 56 | 57 | var ToUpperCase = DOMClass({ 58 | with: Bindings, 59 | template: '{{name}} {{surname}}', 60 | bindings: { 61 | get name() { 62 | return this._name; 63 | }, 64 | set name(value) { 65 | this._name = value.toUpperCase(); 66 | } 67 | } 68 | }); 69 | 70 | var tuc = document.body.insertBefore( 71 | new ToUpperCase, 72 | document.body.firstChild 73 | ); 74 | 75 | 76 | */ 77 | 78 | /* 79 | 80 | var Celsius2Fahrenheit = DOMClass({ 81 | with: Bindings, 82 | name: 'celsius-2-Fahrenheit', 83 | css: { 84 | 'input': { 85 | maxWidth: 64, 86 | border: {width: 1, color: 'silver'} 87 | } 88 | }, 89 | template: '
' + 90 | '°C' + 91 | '' + 92 | '°F' + 93 | '
', 94 | bindings: { 95 | celsius: 32, 96 | fahrenheit: 0 97 | }, 98 | constructor: function () { 99 | this.addEventListener('input', function (e) { 100 | switch (e.target.name) { 101 | case 'celsius': 102 | e.currentTarget.bindings.fahrenheit = 103 | 9/5 * parseFloat(e.target.value) + 32; 104 | break; 105 | case 'fahrenheit': 106 | e.currentTarget.bindings.celsius = 107 | 5/9 * (parseFloat(e.target.value) - 32) 108 | break; 109 | } 110 | }); 111 | }, 112 | updateTemperature: function (which, temp) { 113 | if (which in this.bindings) { 114 | this.bindings[which] = temp; 115 | this.query('[name="' + which + '"]').dispatchEvent( 116 | new CustomEvent('input', {bubbles: true}) 117 | ); 118 | } else { 119 | alert('how to convert ' + which + ' ?'); 120 | } 121 | } 122 | }); 123 | 124 | var c2f = document.body.insertBefore( 125 | new Celsius2Fahrenheit, 126 | document.body.firstChild 127 | ); 128 | 129 | //*/ 130 | 131 | /* 132 | var EditableNameTag = DOMClass({ 133 | with: Bindings, 134 | name: 'editable-name-tag', 135 | template: '

'+ 136 | 'This is a {{owner}}\'s editable-name-tag.' + 137 | '

' + 138 | '', 139 | bindings: {owner: "Daniel"} 140 | }); 141 | 142 | var ent = document.body.insertBefore( 143 | new EditableNameTag, 144 | document.body.firstChild 145 | ); 146 | 147 | function verify() { 148 | console.log( 149 | ent.bindings.owner, 150 | ent.firstChild.getAttribute('custom'), 151 | ent.lastChild.value, 152 | ent.firstChild.childNodes[1].textContent 153 | ); 154 | } 155 | 156 | setTimeout(function () { 157 | ent.bindings.owner = 'Andrea'; 158 | setTimeout(function () { 159 | verify(); 160 | ent.firstChild.setAttribute('custom', 'WebReflection'); 161 | setTimeout(function () { 162 | verify(); 163 | ent.lastChild.value = 'Daniel'; 164 | setTimeout(verify, 1000); 165 | }, 1000); 166 | }, 1000); 167 | }, 1000); 168 | 169 | //*/ 170 | 171 | /* 172 | Bindings.withBindings.call(document.body, { 173 | template: '' + 174 | '', 175 | bindings: {name: 'webreflection', random: 'wut', checked: false} 176 | }); 177 | 178 | setTimeout(function () { 179 | document.body.bindings.name = 'asd'; 180 | document.body.bindings.checked = true; 181 | }, 500); 182 | //*/ 183 | 184 | /* 185 | Bindings.withBindings.call(document.body, { 186 | template: '{{dumber}} and {{dumber}}', 187 | bindings: {dumber: 'dumber'} 188 | }); 189 | 190 | document.body.bindings.dumber = 'yo'; 191 | //*/ 192 | 193 | /* 194 | 195 | var BoundContent = new DOMClass({ 196 | with: Bindings 197 | }); 198 | 199 | var bc = (new BoundContent).withBindings({ 200 | template: '{{text}}', 201 | bindings: {text: 'Hello World'} 202 | }); 203 | 204 | //*/ -------------------------------------------------------------------------------- /demo/x-greeter.js: -------------------------------------------------------------------------------- 1 | var XGreeter = DOMClass({ 2 | 3 | // use bindings mixin 4 | 'with': [DOMClass.bindings], 5 | 6 | // style any internal node with a class strong 7 | css: {'.strong':{fontWeight: 'bold'}}, 8 | 9 | // template, it could be ES6 back-tick multi line string as well 10 | template: ''.concat( 11 | // 12 | // data-bind accepts one or more comma separated bindings. 13 | // Each data binding is a key:value pair 14 | // - the key is the node property or attribute 15 | // - the value is the property name in the bindings object 16 | // The value could also be method invocation. 17 | // In latter case, every time one of its parameters changes 18 | // the method will be re-executed and its return assigned 19 | // on the element as property or attribute. 20 | // example: 21 | '

', 22 | // ↑ ↑ ↑ 23 | // └─────────┤ └─── every time the 'bold' 24 | // │ property changes ┐ 25 | // and the p 'class' │ │ 26 | // attribute changes └── changeClass gets invoked ←┘ 27 | // 28 | // --------------------------------------------------------- 29 | // 30 | // When it comes to text nodes we don't have 31 | // properties or attributes like we do on elements. 32 | // In this case we can either bind a property by name 33 | // or just a method which will update the text node 34 | // content every time one of its parameters changes. 35 | // 36 | 'Hello {{greeter(selectedIndex)}}, and Welcome!', 37 | // ↑ ↑ 38 | // │ └─── every time the property 39 | // it's return │ selectedIndex changes ┐ 40 | // is the text │ │ 41 | // └── the method greeter gets invoked ←┘ 42 | '

', 43 | '

', 59 | '

' 74 | ), 75 | 76 | // the binding object, it's inherited per each instance 77 | // but its bound properties are created on top 78 | // whenever this.createBindings(this.bindings) is called 79 | // The DOMClass.bindings mixin has aumatic mixin:init() 80 | // so that this will be understood and correctly analyzed 81 | // whenever a new instance is created 82 | bindings: { 83 | // reflects the initial ', 329 | bindings: {owner: "Daniel"} 330 | }); 331 | window.ent = document.body.insertBefore( 332 | new EditableNameTag, 333 | document.body.firstChild 334 | ); 335 | setTimeout(function () { 336 | ent.bindings.owner = 'Andrea'; 337 | setTimeout(function () { 338 | verify(); 339 | ent.firstChild.setAttribute('custom', 'WebReflection'); 340 | setTimeout(function () { 341 | verify(); 342 | ent.lastChild.value = 'Daniel'; 343 | setTimeout(function () { 344 | verify(); 345 | done(); 346 | }, 350); 347 | }, 350); 348 | }, 350); 349 | }, 350); 350 | } 351 | }, { 352 | name: 'temperature two way', 353 | test: function () { 354 | var Celsius2Fahrenheit = DOMClass({ 355 | 'with': DOMClass.bindings, 356 | name: 'celsius-2-Fahrenheit', 357 | css: {'input': { 358 | maxWidth: 64, 359 | border: {width: 1, color: 'silver'} 360 | }}, 361 | template: '
' + 362 | '°C' + 363 | '' + 364 | '°F' + 365 | '
', 366 | bindings: { 367 | celsius: 0, 368 | fahrenheit: 0 369 | }, 370 | constructor: function () { 371 | this.query('[name="celsius"]').addEventListener('input', this); 372 | this.query('[name="fahrenheit"]').addEventListener('input', this); 373 | }, 374 | updateTemperature: function (which, temp) { 375 | if (which in this.bindings) { 376 | this.bindings[which] = temp; 377 | this.query('[name="' + which + '"]').dispatchEvent( 378 | new CustomEvent('input') 379 | ); 380 | } else { 381 | alert('dunno how to convert ' + which); 382 | } 383 | }, 384 | // using the class itself to handle events? Why not! 385 | handleEvent: function (e) { 386 | var el = e.currentTarget; 387 | if (e.type === 'input') { 388 | switch (el.name) { 389 | case 'celsius': 390 | this.bindings.fahrenheit = 391 | 9/5 * parseFloat(el.value) + 32; 392 | break; 393 | case 'fahrenheit': 394 | this.bindings.celsius = 395 | 5/9 * (parseFloat(el.value) - 32) 396 | break; 397 | } 398 | } 399 | } 400 | }); 401 | 402 | window.c2f = document.body.insertBefore( 403 | new Celsius2Fahrenheit, 404 | document.body.firstChild 405 | ); 406 | 407 | c2f.updateTemperature('celsius', 30); 408 | 409 | } 410 | }, { 411 | name: 'sum between inputs', 412 | test: function () { 413 | var SumOfTwo = DOMClass({ 414 | 'with': DOMClass.bindings, 415 | template: '' + 416 | '' + 417 | '' + 418 | '
' + 419 | 'The sum of {{a}} + {{b}} is equal to {{sum(a, b)}}', 420 | bindings: {a: 0, b: 0, sum: function (a, b) { 421 | var result = parseFloat(a) + parseFloat(b); 422 | return isNaN(result) ? '?' : result; 423 | }}, 424 | dispatchBindings: true 425 | }); 426 | window.sot = document.body.insertBefore( 427 | new SumOfTwo, 428 | document.body.firstChild 429 | ); 430 | sot.addEventListener('bindingChanged', function (e) { 431 | if (window.console) console.log(e.detail); 432 | }); 433 | } 434 | }, { 435 | name: 'stylesheet attribute', 436 | test: function () { 437 | if (window.__PHANTOMJS__) return; 438 | var StyleSheet = DOMClass({ 439 | name: 'runtime-style-sheet', 440 | stylesheet: 'test/runtime-style-sheet.css', 441 | template: 'Hello lazy blocking runtime style!' 442 | }); 443 | var el = document.body.appendChild(new StyleSheet); 444 | wru.assert('it should have 6px as margin-left', 445 | getComputedStyle(el, null).getPropertyValue('margin-left') === '6px'); 446 | // document.body.removeChild(el); 447 | } 448 | }, { 449 | name: 'styling using extends', 450 | test: function () { 451 | var XButton = new DOMClass({ 452 | extends: HTMLButtonElement, 453 | name: 'x-button', 454 | css: { 455 | '': { 456 | border: '4px solid black' 457 | } 458 | } 459 | }); 460 | var xb = document.body.appendChild(new XButton); 461 | setTimeout(wru.async(function () { 462 | wru.assert('it should have 4px border', 463 | getComputedStyle(xb, null).getPropertyValue('border-width') === '4px'); 464 | document.body.removeChild(xb); 465 | }), 200); 466 | } 467 | } 468 | ]); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dom-class [![build status](https://secure.travis-ci.org/WebReflection/dom-class.svg)](http://travis-ci.org/WebReflection/dom-class) 2 | ========= 3 | 4 | Built on top of [es-class](https://github.com/WebReflection/es-class#es-class-), and integrating handy [restyle](https://github.com/WebReflection/restyle#restyle) features, `DOMClass` is a lightweight, cross browser, simplification of [WebComponents](http://www.w3.org/standards/techs/components#w3c_all). 5 | 6 | 7 | Please read the [related blog post](https://www.webreflection.co.uk/blog/2015/09/17/simplified-web-components-via-dom-class) to know extra details about this project. 8 | 9 | 10 | ### Demo 11 | In case of doubts, some demo could quickly clarify DOMClass potentials so here they are: 12 | 13 | * the (in)famouse [DBMonster](http://webreflection.github.io/dom-class/demo/dbmonster.html) benchmark, a [component](https://github.com/WebReflection/dom-class/blob/master/demo/db-monster.js) split in rows and cells that performs like its pure DOM counter part 14 | * few [classic basic components](http://webreflection.github.io/dom-class/demo/) for forms and playful interaction, or runtime creation 15 | * a one to one [comparison VS Knockout JS](http://webreflection.github.io/dom-class/demo/domclass-vs-knockoutjs/), showing main differences between these two approaches. TL;DR is that `DOMClass` doesn't use `eval` or `Function` and it's based on DRY components rather than magic HTML parsing (which is not necessarily a bad thing but it's surely a different paradigm) 16 | 17 | Finally a quoted comment from [DOMClass.bindings Mixin VS Performance](https://www.webreflection.co.uk/blog/2015/09/21/dom-class-bindings-vs-performance) post 18 | 19 | > This is seriously cool ... so much functionality via so little framework code.It begs the question ... "What the heck are those other frameworks doing that involves so much code?". I especially like that it is based on the existing web platform as opposed to what is promised in V.Next (i.e. ES5,6, etc, etc). Outstanding work. 20 | 21 | 22 | ### How To 23 | All [es-class features](https://github.com/WebReflection/es-class#features-example) are included, and the `constructor` will be automatically used once the component has been initialized. 24 | 25 | ```js 26 | var MyComponent = new DOMClass({ 27 | constructor: function () { 28 | this.textContent = 'Hello World!'; 29 | } 30 | }); 31 | 32 | document.body.append(new MyComponent); 33 | ``` 34 | 35 | While by default new components will be created name-conflict free, it is possible to specify the desired node name via the `name` property. 36 | 37 | ```js 38 | var XAMe = new DOMClass({ 39 | name: 'x-a-me', 40 | constructor: function () { 41 | console.log(this.textContent); 42 | } 43 | }); 44 | 45 | // we can create via new DOMClass 46 | // or we can use the name 47 | document.body.innerHTML = 'Hello Again!'; 48 | ``` 49 | 50 | ### Extending natives 51 | Since version `0.4.0` it is also possible to extend directly extend native and all we need is basically the following: 52 | ```js 53 | var MyInput = DOMClass({ 54 | extends: HTMLInputElement, 55 | // just for demo purpose 56 | constructor: function (value) { 57 | this.value = value; 58 | } 59 | }); 60 | 61 | document.query('form').appendChild( 62 | new MyInput('hello') 63 | ); 64 | ``` 65 | If a native constructor is used, and it's an instance of `HTMLElement`, there's no need to use the `is` property, stick with a name and `DOMClass` will take care of the rest. 66 | 67 | 68 | ### DOMClass.bindings Mixin 69 | Both one and two way bindings are supported, thanks to [the bindings mixin](https://github.com/WebReflection/dom-class/blob/master/src/Bindings.js). It is also possible to update partial text or html content using a different amount of curly brackets. 70 | 71 | ```js 72 | // bringing bindings in 73 | var EasyBindings = DOMClass({ 74 | // bring bindings in as a mixin 75 | with: [DOMClass.bindings], 76 | // bindings will look for an optional template 77 | template: ` 78 |
79 | This will be test: {{textContent}}.
80 | This will be html: {{{htmlContent}}}
81 | While this will call upper(textContent) each time textContent changes: 82 | {{upper(textContent)}}
83 | And this will call upper(htmlContent) each time it changes: 84 | {{{upper(htmlContent)}}} 85 |
86 | `, 87 | // and an optional bindings property 88 | bindings: { 89 | // we are binding textContent and htmlContent 90 | // we can optionally provide their defaults 91 | textContent: 'just text, really', 92 | htmlContent: 'so bring it on, html!', 93 | // we are also binding a method invocation 94 | // that will update its bound "space" when one of its 95 | // parameters changes 96 | upper: function (textOrHTML) { 97 | return textOrHTML.toUpperCase(); 98 | } 99 | // we could add any extra property or method, even if not bound 100 | } 101 | }); 102 | ``` 103 | In order to avoid any sort of conflicts with the component itself, the bindings mixin creates per each new instance a `bindings` property 104 | that will inherit from the one defined in the class so that defaults and methods can simply be shared. 105 | 106 | We can use this property to update the text just like this: 107 | ```js 108 | var eb = document.body.appendChild( 109 | new EasyBindings 110 | ); 111 | 112 | // how can we change those properties? 113 | eb.bindings.textContent = 'is that it?'; 114 | // it will update only yhe text part of the node 115 | 116 | // let's update the html too 117 | eb.bindings.htmlContent = 'but this is awesome!'; 118 | ``` 119 | The bindings property is not directly aware of its owner, which is in the previous example the `new EasyBindings` variable called `eb`. 120 | This peculiarity ensures that data bindings will be data related, and never directly UI one. 121 | It separates concerns between the component itself and the kind of data it needs to take care of. 122 | 123 | 124 | ### Two way attributes binding 125 | We can update some content, but we cannot be updated if the content is manually changed. This is where two way bindings become handy: inputs, selects, nodes and classes, others ... these can be all managed via this mixin. 126 | 127 | Inspired by [KnockoutJS data-bind syntax](http://knockoutjs.com/documentation/binding-syntax.html), `DOMClass.bindings` mixin will also look for `data-bind` attribute and bind properties accordingly. 128 | 129 | This time we'll have any sort of property name on the left, and any sort of value on its right. 130 | ```js 131 | var InputName = DOMClass({ 132 | with: DOMClass.bindings, 133 | template: ` 134 |
135 | Hello {{name}}! 136 | ` 137 | }); 138 | 139 | var me = document.body.appendChild(new InputName); 140 | me.bindings.name = 'Andrea'; 141 | ``` 142 | We can now even change directly `me.firstChild.value = "really?"` or `me.bindings.name = "no way!"` or simply digit our name in the input. 143 | 144 | 145 | ### Computed one way attributes 146 | Like it is for text and html, we can compute those attributes that won't be manually changed. 147 | ```js 148 | var LogIn = DOMClass({ 149 | with: DOMClass.bindings, 150 | template: ` 151 |
152 | 153 |
154 | 155 |
156 | 160 |
161 | `, 162 | bindings: { 163 | unauthorized: function (user, pass) { 164 | return !( 165 | /\S+/.test(user || '') && 166 | /\S{8,}/.test(pass || '') 167 | ); 168 | } 169 | } 170 | }); 171 | 172 | 173 | var login = document.body.appendChild(new LogIn); 174 | // try to use input fields directly or ... 175 | login.bindings.user = 'this should be'; 176 | login.bindings.pass = 'safe-enough'; 177 | 178 | ``` 179 | If any of the parameters in `unauthorized` method gets updated, its DOM counterpart will be updated. 180 | This works within the component or its bindings object, or from the outer world. 181 | 182 | 183 | #### Same attribute and property name 184 | The mixin is smart enough to understand same properties name. If the binding is called value, no need to `value:value`, just `value` would do. 185 | ```js 186 | { 187 | template: ` 188 | 189 | `, 190 | bindings: {value: 'default'} 191 | } 192 | ``` 193 | 194 | 195 | #### Multiple attributes binding per node 196 | Using comma separated value would do. 197 | ```js 198 | { 199 | template: ` 200 | 201 | `, 202 | bindings: { 203 | key: 'some-key-name', 204 | wut: function (a, b) { 205 | return a + b; 206 | }, 207 | worries: 'nope' 208 | } 209 | } 210 | ``` 211 | 212 | ### Dispatching changed bindings 213 | If the `dispatchBindings` property is defined, and it's either truthy or an integer greater than -1, the component will trigger a `bindingChanged` event each time something changed. 214 | ```js 215 | var ChangingInput = DOMClass({ 216 | with: DOMClass.bindings, 217 | dispatchBindings: true, 218 | template: '' 219 | }); 220 | 221 | document.body.appendChild( 222 | new ChangingInput 223 | ).addEventListener( 224 | 'bindingChanged', 225 | console.log.bind(console) 226 | ); 227 | ``` 228 | Every time we type a letter in that input its parent component will dispatch a `CustomEvent` with a `detail` property which will contain at least two properties: `key`, representing the component `bindings[key]` that triggered such notification, and `value`, which will be the newly assigned value to that bindings property. 229 | 230 | Please note that by default, or better setting `dispatchBindings` just as true, there will be a delay between notifications. 231 | 232 | This makes the mechanism less greedy by default but if needed, we can specify an arbitrary amount of milliseconds. 233 | Such amount will be used as `setTimoeut` delay or as `requestAnimationFrame` one in case it's 0, where 0 in this case means *ASAP*. 234 | 235 | Bear in mind if you have a listener and within such lister you change another binding in the same component you are luckily to put yourself into an infinite updating loop: don't mix up UI changes and events notification with data related binding! 236 | 237 | 238 | #### Going dirty with bindings 239 | Of course it would be possible to assign `eb.bindings.self = this` within its constructor, but if we need to directly modify the DOM when data changes and from the inside of a bound method or setter (yes, setters in bindings are supported too) then we might rethink the logic. 240 | There are few sketchy attempts in the demo folder, the `CelsiusToFahrenheitBindingChanged` and the `CelsiusToFahrenheitSeppuku`. These are really not recommended approaches since everything could be solved easily and cleanly via the `CelsiusToFahrenheitInput` one. 241 | 242 | 243 | ### Custom Elements callbacks 244 | All [famous and verbose CustomElement callbacks](http://www.w3.org/TR/custom-elements/#types-of-callbacks) are aliased in a simpler, yet semantic, way. 245 | 246 | ```js 247 | var MyCE = new DOMClass({ 248 | 249 | // equivalent of createdCallback 250 | // will be invoked after the component will get initialized 251 | // in case there are mixins or super extends 252 | constructor: function () {}, 253 | 254 | // equivalent of attachedCallback 255 | onAttached: function () {}, 256 | 257 | // equivalent of detachedCallback 258 | onDetached: function () {}, 259 | 260 | // equivalent of attributeChangedCallback 261 | onChanged: function () {} 262 | 263 | }); 264 | ``` 265 | 266 | It is possible to style elements per Component, which will eventually also create a `css` property we can use to apply own specific styles. 267 | 268 | ```js 269 | var XSquare = new DOMClass({ 270 | name: 'x-square', 271 | css: { 272 | // to self reference the component 273 | // it is also possible to use just empty selector 274 | // handy specially when the name is unknown 275 | 'x-square': { 276 | display: 'block', 277 | padding: 0, 278 | width: 32, 279 | height: 32, 280 | border: '1px solid black' 281 | }, 282 | // to reference any element within the component 283 | // simply use regular CSS selectors 284 | 'span': { 285 | display: 'inline-block', 286 | width: '100%', 287 | lineHeight: 32, 288 | textAlign: 'center', 289 | font: { 290 | size: 26, 291 | weight: 'bold' 292 | } 293 | } 294 | // mediq queries, animations, and everything else 295 | // are also supported. Please see `restyle` documentation 296 | }, 297 | // Yes! DOMClass accepts arguments too \o/ 298 | constructor: function (text) { 299 | this.innerHTML = '' + text + ''; 300 | // if there is a css in the prototype, 301 | // we can use the css property to overwrite/set the inherited one 302 | // following is the equivalent of this.css = { ... } 303 | this.css.set({ 304 | // empty selector, same as using 'x-square' 305 | // to reference itself 306 | '': { 307 | backgroundColor: 'rgb(' + [ 308 | Math.random() * 256 | 0, 309 | Math.random() * 256 | 0, 310 | Math.random() * 256 | 0 311 | ].join(',') + ')' 312 | } 313 | }); 314 | } 315 | }); 316 | 317 | // example 318 | document.body.append(new XSquare('!')); 319 | document.body.append(new XSquare('A')); 320 | document.body.append(new XSquare('Z')); 321 | ``` 322 | 323 | Please note there is no ShadowDOM, template, or HTMLImport polyfill, everything works with regular, well supported, HTML5 standards and on top of [document.registerElement](https://github.com/WebReflection/document-register-element#document-register-element). 324 | 325 | 326 | ### Live demos and benchmarks 327 | In [the following demo page](http://webreflection.github.io/dom-class/demo/) it's possible to interact with components. Read its source code to know more. 328 | 329 | The famous [DBMonster benchmark](http://webreflection.github.io/dom-class/demo/dbmonster.html) is also available for fun and comparison. 330 | All compatible devices can also be tested against this page and the component file is just [this little](https://github.com/WebReflection/dom-class/blob/master/demo/db-monster.js). 331 | 332 | 333 | ### Compatibility 334 | 335 | The following list of **desktop** browsers has been successfully tested: 336 | 337 | * Chrome 338 | * Firefox 339 | * IE9 or greater 340 | * Safari 341 | * Opera 342 | 343 | The following list of **mobile** OS has been successfully tested: 344 | 345 | * iOS 5.1 or greater 346 | * Android 2.2 or greater 347 | * FirefoxOS 1.1 or greater 348 | * KindleFire 3 or greater 349 | * Windows Phone 7 or greater 350 | * Opera Mobile 12 or greater 351 | * Blackberry OS 10 352 | * webOS 2 or LG TV 353 | * Samsung Bada OS 2 or greater 354 | 355 | If you'd like to know if your browser is supported please check the [live test page](http://webreflection.github.io/dom-class/test/) and let me know if something is not green, thank you. 356 | 357 | 358 | ### Which file for what ? 359 | 360 | The [build folder](https://github.com/WebReflection/dom-class/blob/master/build/) contains all variants, following explained: 361 | 362 | * **vanilla** `DOMClass` [minified](https://github.com/WebReflection/dom-class/blob/master/build/dom-class.js) or [magnified](https://github.com/WebReflection/dom-class/blob/master/build/dom-class.max.js), both requires [es-class](https://github.com/WebReflection/es-class) and, if used, [restyle](https://github.com/WebReflection/restyle) 363 | * **browserify** [CommonJS module](https://github.com/WebReflection/dom-class/blob/master/build/dom-class.node.js), which also depends on [es-class](https://github.com/WebReflection/es-class) and [restyle](https://github.com/WebReflection/restyle) 364 | * **AMD** [module](https://github.com/WebReflection/dom-class/blob/master/build/dom-class.amd.js), which also depends on [es-class](https://github.com/WebReflection/es-class) and [restyle](https://github.com/WebReflection/restyle) 365 | * **Vitamer JS** [all in one shot](https://github.com/WebReflection/dom-class/blob/master/build/vitamer.js), which also optionally comes with [query-result](https://github.com/WebReflection/query-result) to simplify common DOM manipulation and operations. Such version is called [vitamer-qr.js](https://github.com/WebReflection/dom-class/blob/master/build/vitamer-qr.js) 366 | * **dom-class-mixins** [minified](https://github.com/WebReflection/dom-class/blob/master/build/dom-class-mixins.js) to bring with DOMClass handy/evergreen mixins like `DOMClass.bindings` 367 | * **Vitamer Mixins** [all in one file](https://github.com/WebReflection/dom-class/blob/master/build/vitamer-mixins.js) used in this repository for all tests and demos too. 368 | * **Vitamer Mixins and $('query-selector')** [all in one file](https://github.com/WebReflection/dom-class/blob/master/build/vitamer-mixins-qr.js), to simplify DOM manipulation on top of all the goodies brought in via `vitamer` 369 | 370 | 371 | ### What is Vitamer JS ? 372 | Directly [from Wikipedia](https://en.wikipedia.org/wiki/Vitamer): 373 | 374 | > Typically, the vitamin activity of multiple vitamers is due to the body's (limited) ability to convert one vitamer to another, or many vitamers to the same enzymatic cofactor(s), which is active in the body as the important form of the vitamin. 375 | > 376 | > As part of the definition of vitamin, the body cannot completely synthesize an optimal amount of vitamin activity from simple foodstuffs, without some minimal amount of a vitamer molecule as a basis. 377 | 378 | In this case it's the minimum amount of packages required in order to obtain a modern, comfortable, and cross browser environment based on latest DOM standards and proposals. 379 | 380 | The "_all in one shot_" file contains the following modules: 381 | 382 | * IE9 only [quick fix](src/ie-lte-9.js) for [standard timers](http://www.w3.org/TR/2011/WD-html5-20110525/timers.html#timers) 383 | * the [dom4](https://github.com/WebReflection/dom4) normalizer 384 | * the [document-register-element](https://github.com/WebReflection/document-register-element) polyfill 385 | * the powerful and handy [restyle](https://github.com/WebReflection/restyle) 386 | * the awesome [es-class](https://github.com/WebReflection/es-class), with lightweight traits capability and many other goodies 387 | * optionally the quick and clean [query-result](https://github.com/WebReflection/query-result) to simplify DOM operations 388 | * optionally DOMClass.mixins such `bindings` and `data` or others 389 | 390 | From vanilla JS world, above package might be truly everything you need in order to create amazing apps, forgetting about cross platform issues or performance gotchas (greedy RAM or CPU operations). 391 | 392 | Since the total package amount, once minified and gzipped, is *less than 9KB*, I thought *Vitamer*, as opposite to the well known *Polymer*, would have worked as file name. Let me know if you have better name ideas :-) 393 | 394 | 395 | 396 | ### License 397 | The MIT Style License 398 | ``` 399 | Copyright (C) 2015 by Andrea Giammarchi - @WebReflection 400 | 401 | Permission is hereby granted, free of charge, to any person obtaining a copy 402 | of this software and associated documentation files (the "Software"), to deal 403 | in the Software without restriction, including without limitation the rights 404 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 405 | copies of the Software, and to permit persons to whom the Software is 406 | furnished to do so, subject to the following conditions: 407 | 408 | The above copyright notice and this permission notice shall be included in 409 | all copies or substantial portions of the Software. 410 | 411 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 412 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 413 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 414 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 415 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 416 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 417 | THE SOFTWARE. 418 | ``` 419 | -------------------------------------------------------------------------------- /demo/domclass-vs-knockoutjs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vitamer Mixins Demo 5 | 6 | 7 | 8 | 9 | 95 | 96 | 97 | 127 | 128 | 129 |
130 |

DOMClass VS Knockout

131 |

132 | This page replicates Knockout examples via DOMClass components. 133 |

134 |

135 | Each example explains main differences between original code [↗] and the one used here. 136 |

137 |

138 | Feel free to check the source code of this page to actually see that WYSIWYG. 139 |

140 |

141 | Please bear in mind that DOMClass is a Web Components like abstraction, which is very different as paradigm to Knockout one. 142 | Also please remember not a single piece of DOMClass code has been evaluated and also there are no repeaters in the templates. 143 | It's a simplified way to create components which are nice and clean and don't need model definitions on the wild but just a class definition to possibly never even interact with directly but through events. 144 |

145 |
146 |

Hello World [↗]

147 |

148 | Main difference is that DOMClass one changes in real time and not only on change event. 149 |

150 | 151 | 166 | 167 |

First name:

168 |

Last name:

169 |

Hello, {{join(firstName, lastName)}}!

170 |
171 |
172 |
173 |
174 |

Click counter [↗]

175 |

176 | Main difference is that we use standard events when we need them. 177 | This gives us the ability to also drop them eventually in some edge case we might encounter. 178 |

179 | 180 | 201 | 202 |
You've clicked {{clicks}} times
203 | 204 |
205 | That's too many clicks! Please stop before you wear out your fingers. 206 | 207 |
208 |
209 |
210 |
211 |
212 |

Simple list [↗]

213 |

214 | The main difference is that DOMClass doesn't have auto-magic behaviors. The bound options, as example, 215 | will be exactly the select.options and instead of using this.items.push(value) we expose a proper addItem method. 216 |

217 |

218 | Another important difference is the ability to pass constructor arguments as JSON Array and through the data-arguments property. 219 | This makes the component usable as pre-rendered HTML, or as simple JS created one: body.append(new SimpleList('a','b')) 220 |

221 | 222 | 245 | 246 | New item: 247 | 248 | 249 |

Your items:

250 | 251 |
252 |
253 |
254 |
255 |

Better list [↗]

256 |

257 | Same differences described in the simple list plus some extra logic for sorting and removing. 258 | Once again, there's no auto-magic behaviors in DOMClass, neither code evaluation. 259 | We need to define what happens by ourselves and here that's defined in the handleEvent. 260 |

261 | 262 | 320 | 321 | Add item: 322 | 323 |

Your values:

324 | 325 |
326 | 327 | 328 |
329 |
330 |
331 |
332 |
333 |

Control types [↗]

334 |

The main difference is that in DOMClass you can invoke methods but you need to specify per which property such method should be invoked.

335 |

Please note this is a very demo-ish exercise and the crossed representation of the same binding in multiple place is not an ideal scenario to define with current bindings logic but also ... hey, it works ;-)

336 |

For the first time in this page we use direct methods bindings too so that onchange:method will invoke bindings.method(event) once it changes.

337 |

Again, this is a demo, so here I am showing off some bindings features but in a component like approach we could just use addEventListener and update bindings once triggered.

338 | 339 | 405 | 406 |
407 |

What's in the model?

408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 |
Text value:{{stringValue}}
Password:{{passwordValue}}
Bool value:{{getBoolTxt(booleanValue)}}
Selected option:{{getSingleSelectValue(selectedIndex)}}
Multi-selected options:{{getMultipleSelectValue(selectedOptions)}}
Radio button selection:{{getRadio(radioAlpha,radioBeta,radioGamma)}}
434 |
435 | 436 |

HTML controls

437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 473 | 474 |
Text value (updates on change):
Text value (updates on keystroke):
Text value (multi-line):
Password:
Checkbox:
Drop-down list:
Multi-select drop-down list:
Radio buttons: 469 | 470 | 471 | 472 |
475 |
476 |
477 |
478 |
479 |

Working with Collections [↗]

480 |

Main difference in this example is that DOMClass does not use eval or Function so it's more procedural.

481 |

Also there are no repeaters in the template, because there are no nested bindings yet.

482 |

The component is split in 3: the main container, its generic Person and eventually children through the Child component.

483 | 484 | 562 | 568 | 569 | 570 |
571 |
572 | 573 | -------------------------------------------------------------------------------- /src/Bindings.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(DOMClass, 'bindings', { 2 | enumerable: true, 3 | value: (function (O) {'use strict'; 4 | 5 | /*! (C) 2015 Andrea Giammarchi - Mit Style License */ 6 | 7 | // I must admit this wasn't fun ... so many quirks! 8 | // works down to Android 2.3, webOS (wut?!) and Windows Phone 7 9 | // BB7 has problems defining properties on DOM Object 10 | // ... I salute you BB7, it's been fun! 11 | 12 | var 13 | // constants 14 | STATE_OFF = 0, 15 | STATE_DIRECT = 1, 16 | STATE_ATTRIBUTE = 2, 17 | STATE_EVENT = 4, 18 | DOM_ATTR_MODIFIED = 'DOMAttrModified', 19 | ON_ATTACHED = 'attachedCallback', 20 | ON_DETACHED = 'detachedCallback', 21 | GET_ATTRIBUTE = 'getAttribute', 22 | SET_ATTRIBUTE = 'setAttribute', 23 | DROP_BINDINGS = 'destroyBindings', 24 | // shortcuts 25 | create = O.create, 26 | dP = O.defineProperty, 27 | gPO = O.getPrototypeOf, 28 | gOPD = O.getOwnPropertyDescriptor, 29 | // RegExp used all the time 30 | ignore = /IFRAME|NOFRAMES|NOSCRIPT|SCRIPT|SELECT|STYLE|TEXTAREA|[a-z]/, 31 | oneWay = /\{\{\{?[\S\s]+?\}\}\}?/g, 32 | oneWayHTML = /^\{[\S\s]+?\}$/, 33 | comma = /\s*,\s*/, 34 | colon = /\s*:\s*/, 35 | spaces = /^\s+|\s+$/g, 36 | decepticons = /^([\S]+?)\(([\S\s]*?)\)/, 37 | // MutationObserver common options 38 | whatToObserve = {attributes: true, subtree: true}, 39 | // used to create HTML 40 | dummy = document.createElement('dummy'), 41 | // moar shortcuts 42 | hOP = whatToObserve.hasOwnProperty, 43 | notNull = function (v, f) { 44 | /* jshint eqnull:true */ 45 | return v == null ? f : v; 46 | }, 47 | on = function (el, type, handler) { 48 | el.addEventListener(type, handler, true); 49 | // ^ ... but why? 50 | // because usually devs set listeners in bubbling 51 | // phase, meaning this one might fire before ^_^ 52 | }, 53 | off = function (el, type, handler) { 54 | el.removeEventListener(type, handler, true); 55 | }, 56 | trim = DOM_ATTR_MODIFIED.trim || function () { 57 | return this.replace(spaces, ''); 58 | }, 59 | createGetSet = function (get, set) { 60 | return { 61 | configurable: true, 62 | enumerable: true, 63 | get: get, 64 | set: set 65 | }; 66 | }, 67 | // if attributes such onchange or others are used 68 | directMethod = function (bindings, method) { 69 | return typeof method === 'function' ? 70 | function () { return method.apply(bindings, arguments); } : 71 | method; 72 | }, 73 | // given an element and a property 74 | // with direct info manipulation capability 75 | // test if there is a descriptor that could be 76 | // used in case it's wrapped/redefined 77 | getInterceptor = function (el, key) { 78 | var proto = el, descriptor; 79 | while (proto && !hOP.call(proto, key)) proto = gPO(proto); 80 | if (proto) { 81 | descriptor = gOPD(proto, key); 82 | if ('set' in descriptor && 'get' in descriptor) { 83 | try { 84 | if (descriptor.get.call(el) !== el[key]) { 85 | throw descriptor; 86 | } 87 | return descriptor; 88 | } catch (iOS) {} 89 | } 90 | } 91 | return null; 92 | }, 93 | MO = window.MutationObserver || 94 | window.WebKitMutationObserver || 95 | window.MozMutationObserver, 96 | hasMo = !!MO, 97 | schedule = function (fn) { 98 | // requestAnimationFrame would be too greedy 99 | // however, it has its advantages like not bothering 100 | // when the window/tab is not focused or something else. 101 | // in this way we are sure we'll reschedule the timer 102 | // only if rAF executes, instead of forever re-scheduled 103 | return setTimeout(requestAnimationFrame, 100, fn); 104 | }, 105 | cancel = clearTimeout, // cancelAnimationFrame 106 | hasDAM = hasMo 107 | ; 108 | 109 | // verify if the DOMAttrModified listener works at all 110 | if (!hasDAM) { 111 | (function verifyItWorks(html, uid) { 112 | function suchAGoodBoy() { hasDAM = true; } 113 | on(html, DOM_ATTR_MODIFIED, suchAGoodBoy); 114 | html[SET_ATTRIBUTE](uid, 1); 115 | html.removeAttribute(uid); 116 | off(html, DOM_ATTR_MODIFIED, suchAGoodBoy); 117 | }( 118 | document.documentElement, 119 | 'dom-' + (Math.random() + '-class').slice(2) 120 | )); 121 | } 122 | 123 | // if there's some textual binding in the wild, like inside 124 | // a generic node, it will be bound "one way" here 125 | function boundTextNode(bindings, key, node, dB, dS) { 126 | var value, setter = hOP.call(bindings, key) && gOPD(bindings, key).set; 127 | dP(bindings, key, createGetSet( 128 | function get() { return value; }, 129 | function set(v) { 130 | node.nodeValue = (value = v); 131 | if (dB) dS(key); 132 | if (setter) setter.call(bindings, v); 133 | } 134 | )); 135 | return node; 136 | } 137 | 138 | // if there's some HTML binding in the wild, like inside 139 | // a generic node, it will be bound "one way" here 140 | function boundFragmentNode(bindings, key, document, innerHTML, dB, dS) { 141 | var 142 | setter = hOP.call(bindings, key) && gOPD(bindings, key).set, 143 | pins = createFragment(document, innerHTML) 144 | ; 145 | dP(bindings, key, createGetSet( 146 | function get() { return innerHTML; }, 147 | function set(value) { 148 | pins = updatePins(document, pins, (innerHTML = value)); 149 | if (dB) dS(key); 150 | if (setter) setter.call(bindings, value); 151 | } 152 | )); 153 | return pins.fragment; 154 | } 155 | 156 | // create a documentFragment adding before and after 157 | // two comments node. There are immune to styles 158 | // but these are also nodes so it's possible later on 159 | // to replace the fragment within these comments ;-) 160 | function createFragment(document, innerHTML) { 161 | var pins, firstChild; 162 | dummy.innerHTML = '' + innerHTML + ''; 163 | pins = { 164 | start: dummy.firstChild, 165 | end: dummy.lastChild, 166 | fragment: document.createDocumentFragment() 167 | }; 168 | while ((firstChild = dummy.firstChild)) 169 | pins.fragment.appendChild(firstChild); 170 | return pins; 171 | } 172 | 173 | // used to replace previous fragment 174 | // will create a new "pins" object with 175 | // comments boundaries and the fragment 176 | function updatePins(document, pins, html) { 177 | var 178 | start = pins.start, 179 | parentNode = start.parentNode, 180 | nextSibling 181 | ; 182 | do { 183 | nextSibling = start.nextSibling; 184 | parentNode.removeChild(nextSibling); 185 | } while (nextSibling !== pins.end); 186 | pins = createFragment(document, html); 187 | parentNode.replaceChild(pins.fragment, start); 188 | return pins; 189 | } 190 | 191 | // if there's some binding dependent to a method it will update 192 | // the node whenever one bound parameter of the method changes 193 | function boundTransformer(source, autobots, bindings, method, keys, document, isHTML) { 194 | var 195 | pins = null, 196 | node = isHTML ? null : document.createTextNode('') 197 | ; 198 | keys.split(comma).forEach(optimusPrime, { 199 | autobots: autobots, 200 | bindings: bindings, 201 | method: source[method], 202 | source: source, 203 | onUpdate: isHTML ? 204 | function (value) { 205 | pins = pins ? 206 | updatePins(document, pins, value) : 207 | createFragment(document, value); 208 | } : 209 | function (value) { 210 | node.nodeValue = value; 211 | } 212 | }); 213 | return node || pins.fragment; 214 | } 215 | 216 | // from a list of property names, returns an Array of values 217 | function getArgs() {/* jshint validthis:true */ 218 | var a = [], i = 0; 219 | while (i < arguments.length) a[i] = this[arguments[i++]]; 220 | return a; 221 | } 222 | 223 | // it controls all autobots, trying to defeat 224 | // all decepticons attack through their callbacks 225 | function optimusPrime(key, i, args) {/* jshint validthis:true */ 226 | var 227 | autobots = this.autobots, 228 | bindings = this.bindings, 229 | source = this.source, 230 | method = this.method, 231 | onUpdate = this.onUpdate, 232 | setter = setGetSetIfAvailable(bindings, source, key).set, 233 | invoke = !!setter 234 | ; 235 | autobots[key] = source[key]; 236 | dP(bindings, key, createGetSet( 237 | function get() { return autobots[key]; }, 238 | function set(value) { 239 | autobots[key] = value; 240 | onUpdate(method.apply(bindings, getArgs.apply(autobots, args))); 241 | if (invoke) setter.call(bindings, value); 242 | } 243 | )); 244 | onUpdate(method.apply(bindings, getArgs.apply(autobots, args))); 245 | } 246 | 247 | // plain borrowed from twemoji ... but that's my code anyway ^_^ 248 | function grabAllTextNodes(node, allText) { 249 | var 250 | childNodes = node.childNodes, 251 | length = childNodes.length, 252 | subnode, 253 | nodeType; 254 | while (length--) { 255 | subnode = childNodes[length]; 256 | nodeType = subnode.nodeType; 257 | if (nodeType === 3) allText.push(subnode); 258 | else if (nodeType === 1 && !ignore.test(subnode.nodeName)) { 259 | grabAllTextNodes(subnode, allText); 260 | } 261 | } 262 | return allText; 263 | } 264 | 265 | // used to understand if and how to dispatch events 266 | function convertShenanigansToNumber(i) { 267 | switch (true) { 268 | case typeof i === 'number': return i < 0 ? -1 : i; 269 | case i: return 133; 270 | default: return -1; 271 | } 272 | } 273 | 274 | // set a binding property to a generic Custom Element 275 | function setBindings(ce, bindings) { 276 | dP(ce, 'bindings', { 277 | configurable: true, 278 | enumerable: true, 279 | writable: false, 280 | value: bindings 281 | }); 282 | } 283 | 284 | // if the source had a getter/setter and 285 | // the target hasn't one descriptor yet 286 | // will copy such descriptor to not loose it 287 | // once it's re-configured 288 | function setGetSetIfAvailable(target, source, key) { 289 | var descriptor; 290 | if (hOP.call(target, key)) { 291 | descriptor = gOPD(target, key); 292 | } else { 293 | descriptor = gOPD(source, key) || whatToObserve; 294 | if (descriptor.set) { 295 | dP(target, key, descriptor); 296 | } 297 | } 298 | return descriptor; 299 | } 300 | 301 | return { 302 | 303 | // if the template is the same per each component 304 | // or there are common bindings, it does everything once created 305 | init: function () { 306 | if (this.template || this.bindings) this.createBindings(this); 307 | }, 308 | 309 | // but it's also possible to lazy attach bindings later on 310 | // please not ce.dropBindings() will be called if already present 311 | createBindings: function (info) { 312 | 313 | // if it's been invoked multiple times, clean up all the things 314 | if (hOP.call(this, DROP_BINDINGS)) this[DROP_BINDINGS](); 315 | 316 | // if the template is there but the node has no content 317 | // meaning it has been probably created via JS 318 | // it will inject the template right away 319 | if (info.template && !trim.call(this.innerHTML)) { 320 | this.innerHTML = info.template; 321 | } 322 | 323 | var 324 | // shortcut and reference 325 | self = this, 326 | // grab current document or use the global one 327 | document = self.ownerDocument || document, 328 | // used to define defaults, actually optional 329 | bindings = info.bindings || {}, 330 | // will find all possible nodes with one-way bindings 331 | textNodes = grabAllTextNodes(self, []), 332 | // holds singular properties values related to nodes or attributes 333 | autobots = create(null), 334 | // maps DOM attribute names => bindings properties name 335 | map = create(null), 336 | // will be actually the exported binding object 337 | values = create(info.bindings || null), 338 | // grab all nodes with some [data-bind="value:prop"] info 339 | attributes = self.queryAll('[data-bind]'), 340 | // used as every DOM_ATTR_MODIFIED handler 341 | dAM = function (e) { 342 | var key = e.attrName, previous = state; 343 | state = STATE_EVENT; 344 | values[map[key]] = e.currentTarget[GET_ATTRIBUTE](key); 345 | state = previous; 346 | }, 347 | // will be updated later on if a MutationObserver is needed 348 | setMO = false, 349 | // default state 350 | state = STATE_OFF, 351 | // NOTIFICATIONS 352 | // if bindings are dispatched, figure out in which frequency 353 | dispatchDelay = convertShenanigansToNumber( 354 | info.dispatchBindings || this.dispatchBindings 355 | ), 356 | // internal boolean flag, if dispatchDelay < 0 don't 357 | dispatchBindings = -1 < dispatchDelay, 358 | // in case there are bindings to dispatch, use a storage to filter notifications 359 | tobeNotified = dispatchBindings && create(null), 360 | // used to schedule and clean up notifications 361 | // the value 0 means ASAP and in that case rAF is used instead of setTimeout 362 | dispatcher = dispatchBindings && function (key) { 363 | delete tobeNotified[key]; 364 | self.dispatchEvent(new CustomEvent('bindingChanged', {detail: { 365 | key: key, 366 | value: values[key] 367 | }})); 368 | }, 369 | // schedules the dispatcher accordingly with the delay 370 | // if the dispatchDelay is 0 will use rAF instead (ASAP) 371 | dispatchScheduler = dispatchDelay ? 372 | function (key) { 373 | if (key in tobeNotified) clearTimeout(tobeNotified[key]); 374 | tobeNotified[key] = setTimeout(dispatcher, dispatchDelay, key); 375 | } : 376 | function (key) { 377 | if (key in tobeNotified) cancelAnimationFrame(tobeNotified[key]); 378 | tobeNotified[key] = requestAnimationFrame(function () { 379 | dispatcher(key); 380 | }); 381 | }, 382 | // whenever it's dropped and nothing else should happe in this scope 383 | dropped = false, 384 | // will be eventually used as MutationObserver instance 385 | mo 386 | ; 387 | 388 | dP(self, DROP_BINDINGS, { 389 | configurable: true, 390 | value: function () { 391 | var key; 392 | // if already dropped get out 393 | if (dropped) return; 394 | // flag it as dropped 395 | dropped = true; 396 | // clean up all scheduled callbacks 397 | if (dispatchBindings) { 398 | for (key in tobeNotified) { 399 | if (dispatchDelay) clearTimeout(tobeNotified[key]); 400 | else cancelAnimationFrame(tobeNotified[key]); 401 | delete tobeNotified[key]; 402 | } 403 | } 404 | // clean up all info, getters, setters, etc 405 | for (key in autobots) delete autobots[key]; 406 | for (key in map) delete map[key]; 407 | for (key in values) delete values[key]; 408 | setBindings(self, {}); 409 | // disconnect the MutationObserver 410 | if (setMO) mo.disconnect(); 411 | // or remove all listeners 412 | else if (hasDAM) attributes.forEach(function (el) { 413 | off(el, DOM_ATTR_MODIFIED, dAM); 414 | }); 415 | // or drop the fake setAttribute method 416 | else attributes.forEach(function (el) { 417 | delete el[SET_ATTRIBUTE]; 418 | }); 419 | } 420 | }); 421 | 422 | // loop over all text nodes 423 | textNodes.forEach(function (node) { 424 | var 425 | isHTML, 426 | k, m, j, l, 427 | i = 0, 428 | value = node.nodeValue, 429 | nodes = [], 430 | bound = [], 431 | parentNode = node.parentNode, 432 | descriptor 433 | ; 434 | while ((m = oneWay.exec(value))) { 435 | j = m.index; 436 | l = m[0].length; 437 | nodes.push(value.slice(i, j)); 438 | bound.push(value.substr(j + 2, l - 4)); 439 | i = j + l; 440 | } 441 | // and in case there was some binding in the wild 442 | if (bound.length) { 443 | // put last part of the loop in the list of nodes 444 | nodes.push(value.slice(i)); 445 | // and append all of them 446 | nodes.forEach(function (text, i) { 447 | // let's ignore empty text nodes 448 | if (text.length) parentNode.insertBefore( 449 | document.createTextNode(text), 450 | node 451 | ); 452 | // bound nodes are always after regular 453 | // unless we are at the end of the list 454 | if (i < bound.length) { 455 | k = trim.call(bound[i]); 456 | // check if this is an HTML intent 457 | isHTML = oneWayHTML.test(k); 458 | if (isHTML) k = k.slice(1, -1); 459 | if ((m = decepticons.exec(k))) { 460 | parentNode.insertBefore( 461 | boundTransformer( 462 | bindings, autobots, values, m[1], m[2], document, isHTML 463 | ), 464 | node 465 | ); 466 | } else { 467 | setGetSetIfAvailable(values, bindings, k); 468 | parentNode.insertBefore( 469 | isHTML ? 470 | boundFragmentNode( 471 | values, k, 472 | document, 473 | notNull(bindings[k], ''), 474 | dispatchBindings, 475 | dispatchScheduler 476 | ) : 477 | boundTextNode( 478 | values, k, 479 | document.createTextNode( 480 | notNull(bindings[k], '') 481 | ), 482 | dispatchBindings, 483 | dispatchScheduler 484 | ), 485 | node 486 | ); 487 | } 488 | } 489 | }); 490 | // drop current node 491 | node.remove(); 492 | } 493 | }); 494 | 495 | attributes.forEach(function (el) { 496 | 497 | var 498 | setAttribute = el[SET_ATTRIBUTE], 499 | fakeSetAttribute = function (key, value) { 500 | var previous = state; 501 | state = STATE_EVENT; // it's a white lie to simulate DAM 502 | setAttribute.call(this, key, value); 503 | // update only known values and only if the context is the right one 504 | // remember: we have one values per component shared only 505 | // with its own sub nodes. If somebody .call via different context 506 | // we don't want the values to be updated 507 | if (key in map && this === el) values[map[key]] = value; 508 | state = previous; 509 | } 510 | ; 511 | 512 | // KnockOut style, data-bind accepts a comma separated list of key/value pairs 513 | el[GET_ATTRIBUTE]('data-bind') 514 | .split(comma) 515 | .filter(function (info, i, all) { 516 | // sanitizes functions with possible multiple arguments 517 | if (info.indexOf('(') > 0 && info.indexOf(')') < 0) { 518 | all[i + 1] = info + ',' + all[i + 1]; 519 | return false; 520 | } 521 | return true; 522 | }).forEach(function (info, i) { 523 | 524 | var 525 | // these pairs can optionally be separated via `:` 526 | pair = info.split(colon), 527 | // so that we always have the related element attribute name 528 | key = pair[0], 529 | // and eventually the corresponding property name on the binding 530 | value = pair[1] || key, 531 | // is this property is one of those "magic" properties ? 532 | // (value for inputs, selectedIndex for selects, checked for checkbox, etc) 533 | direct = key in el, 534 | m = pair[1] && decepticons.exec(value), 535 | handler, 536 | setter, 537 | descriptor, 538 | onAttached, 539 | onDetached, 540 | hasSet, 541 | v 542 | ; 543 | 544 | // one way binding, the attribute does not trigger changes in values 545 | // but it gets updated every time one of its parameters changes 546 | if (m) { 547 | m[2].split(comma).forEach(optimusPrime, { 548 | autobots: autobots, 549 | bindings: values, 550 | method: bindings[m[1]], 551 | source: bindings, 552 | onUpdate: direct ? 553 | function (value) { el[key] = directMethod(values, value); } : 554 | function (value) { setAttribute.call(el, key, value); } 555 | }); 556 | } else { 557 | // handy to bring back property name from an attribute one 558 | map[key] = value; 559 | // if value has already a setter, don't loose it in the process 560 | setter = setGetSetIfAvailable(values, bindings, value).set; 561 | // in case there is a setter, we need to keep that "in mind" 562 | hasSet = !!setter; 563 | // if it's a direct property ... 564 | if (direct) { 565 | // we can simply set it as such using provided defaults 566 | if (hOP.call(bindings, value)) el[key] = 567 | directMethod(values, bindings[value]); 568 | // console.log(gOPD(el, 'key')); 569 | // whenever we set such property via exported bindings 570 | dP(values, value, createGetSet( 571 | // we either return it directly 572 | function get() { return el[key]; }, 573 | // or we set it directly 574 | function set(v) { 575 | var previous = state; 576 | state = STATE_DIRECT; 577 | // console.log('direct', previous, state); 578 | switch (previous) { 579 | case STATE_OFF: 580 | case STATE_EVENT: 581 | el[key] = v; 582 | if (dispatchBindings) dispatchScheduler(value); 583 | break; 584 | } 585 | // if there was already a setter 586 | // we should probably invoke it 587 | if (hasSet) setter.call(values, v); 588 | state = previous; 589 | } 590 | )); 591 | // in few specific cases where the input could change via UI 592 | // we should update exported bindings property too 593 | handler = function (e) { 594 | if (dropped) return off(el, e.type, handler); 595 | var previous = state; 596 | state = STATE_EVENT; 597 | values[value] = el[key]; 598 | state = previous; 599 | }; 600 | switch (key) { 601 | case 'value': 602 | on(el, 'input', handler); 603 | /* falls through */ 604 | case 'checked': 605 | /* falls through */ 606 | case 'selectedIndex': 607 | on(el, 'change', handler); 608 | break; 609 | } 610 | // we also would like to do the same in case 611 | // somebody directly changes the input.value 612 | // or the selectedIndex 613 | descriptor = getInterceptor(el, key); 614 | // if we can reuse the descriptor, we're better off this way 615 | if (descriptor) { 616 | direct = hOP.call(el, key); 617 | dP(el, key, { 618 | configurable: true, 619 | enumerable: descriptor.enumerable, 620 | // get it as it's supposed to be get 621 | get: descriptor.get, 622 | // use the descriptor once set to update the element value 623 | // and also update exported bindings property 624 | set: function (v) { 625 | if (dropped) return direct ? 626 | dP(el, key, descriptor) : delete el[key]; 627 | var previous = state; 628 | state = STATE_ATTRIBUTE; 629 | descriptor.set.call(el, v); 630 | values[value] = v; 631 | state = previous; 632 | } 633 | }); 634 | } 635 | // if we cannot reuse the inherited descriptor 636 | // we unfortunately need some utterly ugly polling fallback 637 | else { 638 | // polling seems to be the best option 639 | v = el[key]; 640 | // each time the following runs ... 641 | handler = function () { 642 | // don't reschedule if dropped 643 | if (dropped) return; 644 | // if direct property access is different 645 | // from the known value 646 | if (el[key] !== v) { 647 | // we update the value 648 | // and the exported bindings 649 | var previous = state; 650 | state = STATE_ATTRIBUTE; 651 | v = el[key]; 652 | values[value] = v; 653 | state = previous; 654 | } 655 | // check again ASAP 656 | i = schedule(handler); 657 | }; 658 | // in order to have a not so greedy schedule 659 | // let's disable scheduling when the component 660 | // is not even on the DOM 661 | // TODO: should this actually run regardless? 662 | onAttached = self[ON_ATTACHED]; 663 | onDetached = self[ON_DETACHED]; 664 | dP(self, ON_ATTACHED, { 665 | configurable: true, 666 | value: function () { 667 | if (!dropped) handler(cancel(i)); 668 | if (onAttached) onAttached.apply(el, arguments); 669 | } 670 | }); 671 | dP(self, ON_DETACHED, { 672 | configurable: true, 673 | value: function () { 674 | if (!dropped) cancel(i); 675 | if (onDetached) onDetached.apply(el, arguments); 676 | } 677 | }); 678 | // let's start checking 679 | handler(); 680 | } 681 | } 682 | // here we are in setAttribute land 683 | else { 684 | // we can use the native method to set default value, if any 685 | if (hOP.call(bindings, value)) setAttribute.call(el, key, bindings[value]); 686 | // now we can set a different operation to update exported bindings 687 | dP(values, value, createGetSet( 688 | // we use getAttribute when accessed 689 | function get() { return el[GET_ATTRIBUTE](key); }, 690 | // and we use native setAttribute when changed 691 | function set(v) { 692 | var previous = state; 693 | state = STATE_ATTRIBUTE; 694 | // console.log('attribute', previous, state); 695 | switch (previous) { 696 | case STATE_OFF: 697 | case STATE_DIRECT: 698 | if (hasMo) mo.disconnect(); 699 | else if(hasDAM) off(el, DOM_ATTR_MODIFIED, dAM); 700 | setAttribute.call(el, key, v); 701 | if (dispatchBindings) dispatchScheduler(value); 702 | if (hasMo) mo.observe(self, whatToObserve); 703 | else if(hasDAM) on(el, DOM_ATTR_MODIFIED, dAM); 704 | break; 705 | } 706 | // here again, if there was already a setter 707 | // we should probably invoke it 708 | if (hasSet) setter.call(values, v); 709 | state = previous; 710 | } 711 | )); 712 | // if we need to know about attributes 713 | // and MutationObserver is available 714 | if (hasMo) setMO = true; // let's use it 715 | // otherwise if DOMAttributeModified works 716 | // let's set a listener 717 | else if (hasDAM) on(el, DOM_ATTR_MODIFIED, dAM); 718 | // otherwise let's set once the new fake method 719 | // it will simulate an event as if it was a DOMAttrModified 720 | else if (el[SET_ATTRIBUTE] !== fakeSetAttribute){ 721 | dP(el, SET_ATTRIBUTE, { 722 | configurable: true, 723 | value: fakeSetAttribute 724 | }); 725 | } 726 | } 727 | } 728 | }); 729 | // if it's' a nested element avoid parsing from parent containers 730 | el.removeAttribute('data-bind'); 731 | }); 732 | 733 | // if MutationObserver is available and 734 | // if there was at least one attribute to listen to 735 | if (setMO) { 736 | // we can recreate a MutationObserver 737 | mo = new MO(function (records) { 738 | var previous = state; 739 | state = STATE_EVENT; 740 | // console.log('Mutation Observer', previous, state); 741 | for (var key, r, i = 0; i < records.length; i++) { 742 | r = records[i]; 743 | // if it's about an attribute 744 | if (r.type === 'attributes') { 745 | key = r.attributeName; 746 | // and there is a key to take care of 747 | // and such key is also well known 748 | /* jshint eqnull:true */ 749 | if (key != null && key in map) { 750 | // we can update the value 751 | values[map[key]] = r.target[GET_ATTRIBUTE](key); 752 | } 753 | } 754 | } 755 | state = previous; 756 | }); 757 | // let's observe the node and its subnodes 758 | mo.observe(self, whatToObserve); 759 | 760 | /* // TODO: should mo stop listening when offline ? 761 | dP(self, 'attachedCallback', { 762 | configurable: true, 763 | value: function () { 764 | mo.observe(self, whatToObserve); 765 | if (onAttached) onAttached.apply(self, arguments); 766 | } 767 | }); 768 | dP(self, 'detachedCallback', { 769 | configurable: true, 770 | value: function () { 771 | mo.disconnect(); 772 | if (onDetached) onDetached.apply(self, arguments); 773 | } 774 | }); 775 | //*/ 776 | 777 | } 778 | 779 | // values object is the one exported as bindings 780 | setBindings(self, values); 781 | 782 | return self; 783 | 784 | } 785 | }; 786 | 787 | }(Object)) 788 | }); -------------------------------------------------------------------------------- /demo/vitamer-mixins.js: -------------------------------------------------------------------------------- 1 | /*! (C) Andrea Giammarchi - @WebReflection - Mit Style License */ 2 | /*@cc_on (function(f){window.setTimeout=f(window.setTimeout);window.setInterval=f(window.setInterval)})(function(f){return function(c,t){var a=[].slice.call(arguments,2);return f(function(){c.apply(this,a)},t)}}); @*/ 3 | (function(e){"use strict";function t(){return c.createDocumentFragment()}function n(e){return c.createElement(e)}function r(e,t){if(!e)throw new Error("Failed to construct "+t+": 1 argument required, but only 0 present.")}function i(e){if(e.length===1)return s(e[0]);for(var n=t(),r=R.call(e),i=0;i3?a(o):null,y=String(o.key),b=String(o.char),w=o.location,E=o.keyCode||(o.keyCode=y)&&y.charCodeAt(0)||0,S=o.charCode||(o.charCode=b)&&b.charCodeAt(0)||0,x=o.bubbles,T=o.cancelable,N=o.repeat,C=o.locale,k=o.view||e,L;o.which||(o.which=o.keyCode);if("initKeyEvent"in u)u.initKeyEvent(t,x,T,k,h,d,p,v,E,S);else if(0>0),s="attached",o="detached",u="extends",a="ADDITION",f="MODIFICATION",l="REMOVAL",c="DOMAttrModified",h="DOMContentLoaded",p="DOMSubtreeModified",d="<",v="=",m=/^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+$/,g=["ANNOTATION-XML","COLOR-PROFILE","FONT-FACE","FONT-FACE-SRC","FONT-FACE-URI","FONT-FACE-FORMAT","FONT-FACE-NAME","MISSING-GLYPH"],y=[],b=[],w="",E=t.documentElement,S=y.indexOf||function(e){for(var t=this.length;t--&&this[t]!==e;);return t},x=n.prototype,T=x.hasOwnProperty,N=x.isPrototypeOf,C=n.defineProperty,k=n.getOwnPropertyDescriptor,L=n.getOwnPropertyNames,A=n.getPrototypeOf,O=n.setPrototypeOf,M=!!n.__proto__,_=n.create||function mt(e){return e?(mt.prototype=e,new mt):this},D=O||(M?function(e,t){return e.__proto__=t,e}:L&&k?function(){function e(e,t){for(var n,r=L(t),i=0,s=r.length;i",n={start:x.firstChild,end:x.lastChild,fragment:e.createDocumentFragment()};while(r=x.firstChild)n.fragment.appendChild(r);return n}function q(e,t,n){var r=t.start,i=r.parentNode,s;do s=r.nextSibling,i.removeChild(s);while(s!==t.end);return t=I(e,n),i.replaceChild(t.fragment,r),t}function R(e,t,n,r,i,s,o){var u=null,a=o?null:s.createTextNode("");return i.split(y).forEach(z,{autobots:t,bindings:n,method:e[r],source:e,onUpdate:o?function(e){u=u?q(s,u,e):I(s,e)}:function(e){a.nodeValue=e}}),a||u.fragment}function U(){var e=[],t=0;while(t0&&e.indexOf(")")<0?(n[t+1]=e+","+n[t+1],!1):!0}).forEach(function(d,m){var g=d.split(b),w=g[0],N=g[1]||w,L=w in e,_=g[1]&&E.exec(N),j,F,R,U,W,X,V;if(_)_[2].split(y).forEach(z,{autobots:x,bindings:q,method:v[_[1]],source:v,onUpdate:L?function(t){e[w]=O(q,t)}:function(t){l.call(e,w,t)}});else{I[w]=N,F=$(q,v,N).set,X=!!F;if(L){T.call(v,N)&&(e[w]=O(q,v[N])),h(q,N,A(function(){return e[w]},function(s){var o=Q;Q=n;switch(o){case t:case i:e[w]=s,Y&&tt(N)}X&&F.call(q,s),Q=o})),j=function(t){if(nt)return k(e,t.type,j);var n=Q;Q=i,q[N]=e[w],Q=n};switch(w){case"value":C(e,"input",j);case"checked":case"selectedIndex":C(e,"change",j)}R=M(e,w),R?(L=T.call(e,w),h(e,w,{configurable:!0,enumerable:R.enumerable,get:R.get,set:function(t){if(nt)return L?h(e,w,R):delete e[w];var n=Q;Q=r,R.set.call(e,t),q[N]=t,Q=n}})):(V=e[w],j=function(){if(nt)return;if(e[w]!==V){var t=Q;Q=r,V=e[w],q[N]=V,Q=t}m=P(j)},U=p[o],W=p[u],h(p,o,{configurable:!0,value:function(){nt||j(H(m)),U&&U.apply(e,arguments)}}),h(p,u,{configurable:!0,value:function(){nt||H(m),W&&W.apply(e,arguments)}}),j())}else T.call(v,N)&&l.call(e,w,v[N]),h(q,N,A(function(){return e[a](w)},function(o){var u=Q;Q=r;switch(u){case t:case n:D?rt.disconnect():B&&k(e,s,J),l.call(e,w,o),Y&&tt(N),D?rt.observe(p,S):B&&C(e,s,J)}X&&F.call(q,o),Q=u})),D?K=!0:B?C(e,s,J):e[f]!==c&&h(e,f,{configurable:!0,value:c})}}),e.removeAttribute("data-bind")}),K&&(rt=new _(function(e){var t=Q;Q=i;for(var n,r,s=0;s