Skip to content

Instantly share code, notes, and snippets.

@larshisken
Created February 2, 2018 10:10
Show Gist options
  • Select an option

  • Save larshisken/7d36f0890c4ca74777b3c19d94cfabd3 to your computer and use it in GitHub Desktop.

Select an option

Save larshisken/7d36f0890c4ca74777b3c19d94cfabd3 to your computer and use it in GitHub Desktop.

Revisions

  1. larshisken created this gist Feb 2, 2018.
    416 changes: 416 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,416 @@
    <!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>
    188 changes: 188 additions & 0 deletions jsbin.gowuhu.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,188 @@
    /**
    * 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.';
    });
    })();