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.
JS Bin // source https://jsbin.com/gowuhu
<!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>
/**
* 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