Created
February 2, 2018 10:10
-
-
Save larshisken/7d36f0890c4ca74777b3c19d94cfabd3 to your computer and use it in GitHub Desktop.
JS Bin // source https://jsbin.com/gowuhu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width"> | |
| <title>JS Bin</title> | |
| <script src="https://ajax.googleapis.com/ajax/libs/prototype/1/prototype.js"></script> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <h1>Two way binding</h1> | |
| <section> | |
| <h1 bind="title"></h1> | |
| <input type="text" bind="title"> | |
| </section> | |
| <section> | |
| <p>Checkbox value is: <span bind="checked"></span></p> | |
| <input type="checkbox" id="checkbox" bind="checked"> | |
| <label for="checkbox">Use a checkbox</label> | |
| </section> | |
| <section> | |
| <p bind="paragraph"></p> | |
| <textarea bind="paragraph" rows="5" cols="30"></textarea> | |
| </section> | |
| <section> | |
| <p>Make changes using a handler</p> | |
| <button id="#action">Click me</button> | |
| </section> | |
| </div> | |
| <script id="jsbin-javascript"> | |
| /** | |
| * A JavaScript module used for two way data binding | |
| * | |
| * @author Lars Hisken <[email protected]> | |
| * | |
| * IMPROVEMENTS: Re-apply bindings on DOM changes | |
| * Properly ship this module | |
| * Write unit tests using eg mocha and jsom | |
| * | |
| * @param {String} root The root component selector | |
| * @param {Object} options The module's options | |
| * @return {Object} | |
| */ | |
| 'use strict'; | |
| function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } | |
| function vmModule() { | |
| var root = arguments.length <= 0 || arguments[0] === undefined ? '#app' : arguments[0]; | |
| var _ref = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | |
| var _ref$bindingAttr = _ref.bindingAttr; | |
| var bindingAttr = _ref$bindingAttr === undefined ? 'bind' : _ref$bindingAttr; | |
| var _ref$inputElements = _ref.inputElements; | |
| var inputElements = _ref$inputElements === undefined ? ['INPUT', 'TEXTAREA'] : _ref$inputElements; | |
| /** | |
| * Select all bound elements inside the root | |
| * TODO: The elementList should have some refresh mechanism when the DOM changes | |
| * @type {NodeList} | |
| */ | |
| var elementList = document.querySelector(root).querySelectorAll('*[' + bindingAttr + ']'); | |
| /** | |
| * Collect all elements by their binding | |
| * TODO: The view object should have some refresh mechanism when the DOM changes | |
| * @type {Object} | |
| */ | |
| var view = Array.prototype.reduce.call(elementList, viewReducer, {}); | |
| /** | |
| * Create the scope object and trigger apply() on property assignment | |
| * TODO: Add a polyfill for wider browser support (eg https://github.com/GoogleChrome/proxy-polyfill) | |
| * @type {Proxy} | |
| */ | |
| var scope = new Proxy({}, { | |
| set: function set(target, name, value) { | |
| target[name] = value; | |
| apply(view, name, value); | |
| } | |
| }); | |
| /** | |
| * Add event listeners to all input elements | |
| * TODO: There should be some method to remove these | |
| */ | |
| Array.prototype.filter.call(elementList, propertyInList('tagName', inputElements)).forEach(function (element) { | |
| // TODO: Different elements require different listeners | |
| // Add support for more elements and test thorougly | |
| if (element.getAttribute('type') === 'checkbox') { | |
| element.addEventListener('change', checkboxHandler); | |
| } else { | |
| element.addEventListener('input', textHandler); | |
| } | |
| }); | |
| /** | |
| * Apply a property value to the view | |
| * @param {Proxy} scope | |
| * @param {String} property | |
| * @param {String} value | |
| * @return {void} | |
| */ | |
| function apply(view, property, value) { | |
| view[property].forEach(function (element) { | |
| // TODO: Different elements require different attributes to be applied | |
| // Add support for more elements and test thorougly | |
| if (checkboxElementNeedsBinding(element, value)) { | |
| element.checked = value; | |
| } else if (textElementNeedsBinding(element, value)) { | |
| element.value = value; | |
| } else if (elementNeedsBinding(element, value)) { | |
| element.textContent = value; | |
| } | |
| }); | |
| } | |
| /** | |
| * Check whether a checkbox input element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function checkboxElementNeedsBinding(element, value) { | |
| return inputElements.indexOf(element.tagName) !== -1 && element.getAttribute('type') === 'checkbox' && element.checked !== value; | |
| } | |
| /** | |
| * Check whether a text input element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function textElementNeedsBinding(element, value) { | |
| return inputElements.indexOf(element.tagName) !== -1 && element.getAttribute('type') === 'text' || element.tagName === 'TEXTAREA' && element.getAttribute('value') !== value; | |
| } | |
| /** | |
| * Check whether an element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function elementNeedsBinding(element, value) { | |
| return element.textContent !== value; | |
| } | |
| /** | |
| * A handler used to assign input values to the scope | |
| * @param {InputEvent} event | |
| * @return {void} | |
| */ | |
| function textHandler(event) { | |
| scope[event.target.getAttribute(bindingAttr)] = event.target.value; | |
| } | |
| /** | |
| * A handler used to assign checkbox values to the scope | |
| * @param {ChangeEvent} event | |
| * @return {void} | |
| */ | |
| function checkboxHandler(event) { | |
| scope[event.target.getAttribute(bindingAttr)] = event.target.checked; | |
| } | |
| /** | |
| * A reducer used to collect elements by their binding | |
| * @param {Object} view The view's accumulator | |
| * @param {Element} element | |
| * @return {Object} | |
| */ | |
| function viewReducer(view, element) { | |
| var bindingValue = element.getAttribute(bindingAttr); | |
| view[bindingValue] = [element].concat(_toConsumableArray(view[bindingValue])); | |
| return view; | |
| } | |
| /** | |
| * Check whether an item's property is in a list | |
| * @param {string} property | |
| * @param {Array} list | |
| * @return {Function} | |
| */ | |
| function propertyInList(property, list) { | |
| return function (item) { | |
| return list.indexOf(item[property]) !== -1; | |
| }; | |
| } | |
| return { view: view, scope: scope, apply: apply }; | |
| } | |
| // Example usage | |
| (function () { | |
| var vm = vmModule(); | |
| // Add some data to the scope | |
| vm.scope.title = 'Use a text input'; | |
| vm.scope.paragraph = 'Use a textarea as input'; | |
| vm.scope.checked = true; | |
| var button = document.getElementById('#action'); | |
| button.addEventListener('click', function (event) { | |
| // Change the scope by assigning values directly | |
| vm.scope.title = 'Well, hello.'; | |
| vm.scope.paragraph = 'Some long description...'; | |
| // Or use apply | |
| vm.apply(vm.view, 'checked', false); | |
| // New values can also be assigned | |
| vm.scope.error = 'An error occurred.'; | |
| }); | |
| })(); | |
| </script> | |
| <script id="jsbin-source-javascript" type="text/javascript">/** | |
| * A JavaScript module used for two way data binding | |
| * | |
| * @author Lars Hisken <[email protected]> | |
| * | |
| * IMPROVEMENTS: Re-apply bindings on DOM changes | |
| * Properly ship this module | |
| * Write unit tests using eg mocha and jsom | |
| * | |
| * @param {String} root The root component selector | |
| * @param {Object} options The module's options | |
| * @return {Object} | |
| */ | |
| function vmModule( | |
| root = '#app', | |
| { bindingAttr = 'bind', inputElements = ['INPUT', 'TEXTAREA'] } = {} | |
| ) { | |
| /** | |
| * Select all bound elements inside the root | |
| * TODO: The elementList should have some refresh mechanism when the DOM changes | |
| * @type {NodeList} | |
| */ | |
| const elementList = document | |
| .querySelector(root) | |
| .querySelectorAll(`*[${bindingAttr}]`); | |
| /** | |
| * Collect all elements by their binding | |
| * TODO: The view object should have some refresh mechanism when the DOM changes | |
| * @type {Object} | |
| */ | |
| const view = Array.prototype.reduce.call(elementList, viewReducer, {}); | |
| /** | |
| * Create the scope object and trigger apply() on property assignment | |
| * TODO: Add a polyfill for wider browser support (eg https://github.com/GoogleChrome/proxy-polyfill) | |
| * @type {Proxy} | |
| */ | |
| const scope = new Proxy( | |
| {}, | |
| { | |
| set: (target, name, value) => { | |
| target[name] = value; | |
| apply(view, name, value); | |
| } | |
| } | |
| ); | |
| /** | |
| * Add event listeners to all input elements | |
| * TODO: There should be some method to remove these | |
| */ | |
| Array.prototype.filter | |
| .call(elementList, propertyInList('tagName', inputElements)) | |
| .forEach(element => { | |
| // TODO: Different elements require different listeners | |
| // Add support for more elements and test thorougly | |
| if (element.getAttribute('type') === 'checkbox') { | |
| element.addEventListener('change', checkboxHandler); | |
| } else { | |
| element.addEventListener('input', textHandler); | |
| } | |
| }); | |
| /** | |
| * Apply a property value to the view | |
| * @param {Proxy} scope | |
| * @param {String} property | |
| * @param {String} value | |
| * @return {void} | |
| */ | |
| function apply(view, property, value) { | |
| view[property].forEach(element => { | |
| // TODO: Different elements require different attributes to be applied | |
| // Add support for more elements and test thorougly | |
| if (checkboxElementNeedsBinding(element, value)) { | |
| element.checked = value; | |
| } else if (textElementNeedsBinding(element, value)) { | |
| element.value = value; | |
| } else if (elementNeedsBinding(element, value)) { | |
| element.textContent = value; | |
| } | |
| }); | |
| } | |
| /** | |
| * Check whether a checkbox input element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function checkboxElementNeedsBinding(element, value) { | |
| return ( | |
| inputElements.indexOf(element.tagName) !== -1 && | |
| element.getAttribute('type') === 'checkbox' && | |
| element.checked !== value | |
| ); | |
| } | |
| /** | |
| * Check whether a text input element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function textElementNeedsBinding(element, value) { | |
| return ( | |
| (inputElements.indexOf(element.tagName) !== -1 && | |
| element.getAttribute('type') === 'text') || | |
| (element.tagName === 'TEXTAREA' && | |
| element.getAttribute('value') !== value) | |
| ); | |
| } | |
| /** | |
| * Check whether an element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function elementNeedsBinding(element, value) { | |
| return element.textContent !== value; | |
| } | |
| /** | |
| * A handler used to assign input values to the scope | |
| * @param {InputEvent} event | |
| * @return {void} | |
| */ | |
| function textHandler(event) { | |
| scope[event.target.getAttribute(bindingAttr)] = event.target.value; | |
| } | |
| /** | |
| * A handler used to assign checkbox values to the scope | |
| * @param {ChangeEvent} event | |
| * @return {void} | |
| */ | |
| function checkboxHandler(event) { | |
| scope[event.target.getAttribute(bindingAttr)] = event.target.checked; | |
| } | |
| /** | |
| * A reducer used to collect elements by their binding | |
| * @param {Object} view The view's accumulator | |
| * @param {Element} element | |
| * @return {Object} | |
| */ | |
| function viewReducer(view, element) { | |
| const bindingValue = element.getAttribute(bindingAttr); | |
| view[bindingValue] = [element, ...view[bindingValue]]; | |
| return view; | |
| } | |
| /** | |
| * Check whether an item's property is in a list | |
| * @param {string} property | |
| * @param {Array} list | |
| * @return {Function} | |
| */ | |
| function propertyInList(property, list) { | |
| return item => list.indexOf(item[property]) !== -1; | |
| } | |
| return { view, scope, apply }; | |
| } | |
| // Example usage | |
| (function() { | |
| const vm = vmModule(); | |
| // Add some data to the scope | |
| vm.scope.title = 'Use a text input'; | |
| vm.scope.paragraph = 'Use a textarea as input'; | |
| vm.scope.checked = true; | |
| const button = document.getElementById('#action'); | |
| button.addEventListener('click', event => { | |
| // Change the scope by assigning values directly | |
| vm.scope.title = 'Well, hello.'; | |
| vm.scope.paragraph = 'Some long description...'; | |
| // Or use apply | |
| vm.apply(vm.view, 'checked', false); | |
| // New values can also be assigned | |
| vm.scope.error = 'An error occurred.'; | |
| }); | |
| })(); | |
| </script></body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * A JavaScript module used for two way data binding | |
| * | |
| * @author Lars Hisken <[email protected]> | |
| * | |
| * IMPROVEMENTS: Re-apply bindings on DOM changes | |
| * Properly ship this module | |
| * Write unit tests using eg mocha and jsom | |
| * | |
| * @param {String} root The root component selector | |
| * @param {Object} options The module's options | |
| * @return {Object} | |
| */ | |
| 'use strict'; | |
| function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } | |
| function vmModule() { | |
| var root = arguments.length <= 0 || arguments[0] === undefined ? '#app' : arguments[0]; | |
| var _ref = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | |
| var _ref$bindingAttr = _ref.bindingAttr; | |
| var bindingAttr = _ref$bindingAttr === undefined ? 'bind' : _ref$bindingAttr; | |
| var _ref$inputElements = _ref.inputElements; | |
| var inputElements = _ref$inputElements === undefined ? ['INPUT', 'TEXTAREA'] : _ref$inputElements; | |
| /** | |
| * Select all bound elements inside the root | |
| * TODO: The elementList should have some refresh mechanism when the DOM changes | |
| * @type {NodeList} | |
| */ | |
| var elementList = document.querySelector(root).querySelectorAll('*[' + bindingAttr + ']'); | |
| /** | |
| * Collect all elements by their binding | |
| * TODO: The view object should have some refresh mechanism when the DOM changes | |
| * @type {Object} | |
| */ | |
| var view = Array.prototype.reduce.call(elementList, viewReducer, {}); | |
| /** | |
| * Create the scope object and trigger apply() on property assignment | |
| * TODO: Add a polyfill for wider browser support (eg https://github.com/GoogleChrome/proxy-polyfill) | |
| * @type {Proxy} | |
| */ | |
| var scope = new Proxy({}, { | |
| set: function set(target, name, value) { | |
| target[name] = value; | |
| apply(view, name, value); | |
| } | |
| }); | |
| /** | |
| * Add event listeners to all input elements | |
| * TODO: There should be some method to remove these | |
| */ | |
| Array.prototype.filter.call(elementList, propertyInList('tagName', inputElements)).forEach(function (element) { | |
| // TODO: Different elements require different listeners | |
| // Add support for more elements and test thorougly | |
| if (element.getAttribute('type') === 'checkbox') { | |
| element.addEventListener('change', checkboxHandler); | |
| } else { | |
| element.addEventListener('input', textHandler); | |
| } | |
| }); | |
| /** | |
| * Apply a property value to the view | |
| * @param {Proxy} scope | |
| * @param {String} property | |
| * @param {String} value | |
| * @return {void} | |
| */ | |
| function apply(view, property, value) { | |
| view[property].forEach(function (element) { | |
| // TODO: Different elements require different attributes to be applied | |
| // Add support for more elements and test thorougly | |
| if (checkboxElementNeedsBinding(element, value)) { | |
| element.checked = value; | |
| } else if (textElementNeedsBinding(element, value)) { | |
| element.value = value; | |
| } else if (elementNeedsBinding(element, value)) { | |
| element.textContent = value; | |
| } | |
| }); | |
| } | |
| /** | |
| * Check whether a checkbox input element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function checkboxElementNeedsBinding(element, value) { | |
| return inputElements.indexOf(element.tagName) !== -1 && element.getAttribute('type') === 'checkbox' && element.checked !== value; | |
| } | |
| /** | |
| * Check whether a text input element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function textElementNeedsBinding(element, value) { | |
| return inputElements.indexOf(element.tagName) !== -1 && element.getAttribute('type') === 'text' || element.tagName === 'TEXTAREA' && element.getAttribute('value') !== value; | |
| } | |
| /** | |
| * Check whether an element needs to be updated | |
| * @param {Element} element | |
| * @param {any} value | |
| * @return {boolean} | |
| */ | |
| function elementNeedsBinding(element, value) { | |
| return element.textContent !== value; | |
| } | |
| /** | |
| * A handler used to assign input values to the scope | |
| * @param {InputEvent} event | |
| * @return {void} | |
| */ | |
| function textHandler(event) { | |
| scope[event.target.getAttribute(bindingAttr)] = event.target.value; | |
| } | |
| /** | |
| * A handler used to assign checkbox values to the scope | |
| * @param {ChangeEvent} event | |
| * @return {void} | |
| */ | |
| function checkboxHandler(event) { | |
| scope[event.target.getAttribute(bindingAttr)] = event.target.checked; | |
| } | |
| /** | |
| * A reducer used to collect elements by their binding | |
| * @param {Object} view The view's accumulator | |
| * @param {Element} element | |
| * @return {Object} | |
| */ | |
| function viewReducer(view, element) { | |
| var bindingValue = element.getAttribute(bindingAttr); | |
| view[bindingValue] = [element].concat(_toConsumableArray(view[bindingValue])); | |
| return view; | |
| } | |
| /** | |
| * Check whether an item's property is in a list | |
| * @param {string} property | |
| * @param {Array} list | |
| * @return {Function} | |
| */ | |
| function propertyInList(property, list) { | |
| return function (item) { | |
| return list.indexOf(item[property]) !== -1; | |
| }; | |
| } | |
| return { view: view, scope: scope, apply: apply }; | |
| } | |
| // Example usage | |
| (function () { | |
| var vm = vmModule(); | |
| // Add some data to the scope | |
| vm.scope.title = 'Use a text input'; | |
| vm.scope.paragraph = 'Use a textarea as input'; | |
| vm.scope.checked = true; | |
| var button = document.getElementById('#action'); | |
| button.addEventListener('click', function (event) { | |
| // Change the scope by assigning values directly | |
| vm.scope.title = 'Well, hello.'; | |
| vm.scope.paragraph = 'Some long description...'; | |
| // Or use apply | |
| vm.apply(vm.view, 'checked', false); | |
| // New values can also be assigned | |
| vm.scope.error = 'An error occurred.'; | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment