From 0a09c0c97649ba3507e25c03329a609d9dd5f99f Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Thu, 12 Dec 2013 10:10:45 -0800 Subject: [PATCH] Node.bind(name, observable) This patch changes the Node.bind so that bind always takes a binding name and an observable. The implication of this is that creating observers is now always the concern of TemplateBinding (and in some case the registered delegate). Note that NodeBinding is now gone. The observable is now serving the same purpose the NodeBinding was. R=arv BUG= Review URL: https://codereview.appspot.com/40490043 --- src/NodeBind.js | 371 +++++++++++++++++++++++------------------------- tests/tests.js | 83 ++++++----- 2 files changed, 216 insertions(+), 238 deletions(-) diff --git a/src/NodeBind.js b/src/NodeBind.js index 1729f20..aca78cc 100644 --- a/src/NodeBind.js +++ b/src/NodeBind.js @@ -53,19 +53,27 @@ } } - Node.prototype.bind = function(name, model, path) { - console.error('Unhandled binding to Node: ', this, name, model, path); + Node.prototype.bind = function(name, observable) { + console.error('Unhandled binding to Node: ', this, name, observable); }; - Node.prototype.unbind = function(name) { - if (!this.bindings) - this.bindings = {}; - var binding = this.bindings[name]; + function unbind(node, name) { + var bindings = node.bindings; + if (!bindings) { + node.bindings = {}; + return; + } + + var binding = bindings[name]; if (!binding) return; - if (typeof binding.close === 'function') - binding.close(); - this.bindings[name] = undefined; + + binding.close(); + bindings[name] = undefined; + } + + Node.prototype.unbind = function(name) { + unbind(this, name); }; Node.prototype.unbindAll = function() { @@ -81,97 +89,60 @@ this.bindings = {}; }; - var valuePath = Path.get('value'); - - function NodeBinding(node, property, model, path) { - this.closed = false; - this.node = node; - this.property = property; - this.model = model; - this.path = Path.get(path); - if ((this.model instanceof PathObserver || - this.model instanceof CompoundPathObserver) && - this.path === valuePath) { - this.observer = this.model; - this.observer.target = this; - this.observer.callback = this.valueChanged; - } else { - this.observer = new PathObserver(this.model, this.path, - this.valueChanged, - this); - } - this.valueChanged(this.value); + function sanitizeValue(value) { + return value === undefined || value === null ? '' : value; } - NodeBinding.prototype = { - valueChanged: function(value) { - this.node[this.property] = this.sanitizeBoundValue(value); - }, - - sanitizeBoundValue: function(value) { - return value == undefined ? '' : String(value); - }, + function updateText(node, value) { + node.data = sanitizeValue(value); + } - close: function() { - if (this.closed) - return; - this.observer.close(); - this.observer = undefined; - this.node = undefined; - this.model = undefined; - this.closed = true; - }, - - get value() { - return this.observer.value; - }, - - set value(value) { - this.observer.setValue(value); - }, - - reset: function() { - this.observer.reset(); - } - }; + function textBinding(node) { + return function(value) { + return updateText(node, value); + }; + } - Text.prototype.bind = function(name, model, path) { + Text.prototype.bind = function(name, observable) { if (name !== 'textContent') - return Node.prototype.bind.call(this, name, model, path); + return Node.prototype.bind.call(this, name, observable); - this.unbind(name); - return this.bindings[name] = new NodeBinding(this, 'data', model, path); + unbind(this, 'textContent'); + updateText(this, observable.open(textBinding(this))); + return this.bindings.textContent = observable; } - function AttributeBinding(element, attributeName, model, path) { - this.conditional = attributeName[attributeName.length - 1] == '?'; - if (this.conditional) { - element.removeAttribute(attributeName); - attributeName = attributeName.slice(0, -1); + function updateAttribute(el, name, conditional, value) { + if (conditional) { + if (value) + el.setAttribute(name, ''); + else + el.removeAttribute(name); + return; } - NodeBinding.call(this, element, attributeName, model, path); + el.setAttribute(name, sanitizeValue(value)); } - AttributeBinding.prototype = createObject({ - __proto__: NodeBinding.prototype, - - valueChanged: function(value) { - if (this.conditional) { - if (value) - this.node.setAttribute(this.property, ''); - else - this.node.removeAttribute(this.property); - return; - } + function attributeBinding(el, name, conditional) { + return function(value) { + updateAttribute(el, name, conditional, value); + }; + } - this.node.setAttribute(this.property, this.sanitizeBoundValue(value)); + Element.prototype.bind = function(name, observable) { + var conditional = name[name.length - 1] == '?'; + if (conditional) { + this.removeAttribute(name); + name = name.slice(0, -1); } - }); - Element.prototype.bind = function(name, model, path) { - this.unbind(name); - return this.bindings[name] = new AttributeBinding(this, name, model, path); + unbind(this, name); + + updateAttribute(this, name, conditional, + observable.open(attributeBinding(this, name, conditional))); + + return this.bindings[name] = observable; }; var checkboxEventType; @@ -214,36 +185,44 @@ } } - function InputBinding(node, property, model, path) { - NodeBinding.call(this, node, property, model, path); - this.eventType = getEventForInputType(this.node); - this.boundNodeValueChanged = this.nodeValueChanged.bind(this); - this.node.addEventListener(this.eventType, this.boundNodeValueChanged, - true); + function updateInput(input, property, value, santizeFn) { + input[property] = (santizeFn || sanitizeValue)(value); } - InputBinding.prototype = createObject({ - __proto__: NodeBinding.prototype, + function inputBinding(input, property, santizeFn) { + return function(value) { + return updateInput(input, property, value, santizeFn); + } + } - nodeValueChanged: function() { - this.value = this.node[this.property]; - this.reset(); - this.postUpdateBinding(); - Platform.performMicrotaskCheckpoint(); - }, + function noop() {} - postUpdateBinding: function() {}, + function bindInputEvent(input, property, observable, postEventFn) { + var eventType = getEventForInputType(input); - close: function() { - if (this.closed) + function eventHandler() { + observable.setValue(input[property]); + observable.reset(); + (postEventFn || noop)(input); + Platform.performMicrotaskCheckpoint(); + } + input.addEventListener(eventType, eventHandler); + + var capturedClose = observable.close; + observable.close = function() { + if (!capturedClose) return; + input.removeEventListener(eventType, eventHandler); - this.node.removeEventListener(this.eventType, - this.boundNodeValueChanged, - true); - NodeBinding.prototype.close.call(this); + observable.close = capturedClose; + observable.close(); + capturedClose = undefined; } - }); + } + + function booleanSanitize(value) { + return Boolean(value); + } // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. // Returns an array containing all radio buttons other than |element| that @@ -274,124 +253,123 @@ } } - function CheckedBinding(element, model, path) { - InputBinding.call(this, element, 'checked', model, path); - } - - CheckedBinding.prototype = createObject({ - __proto__: InputBinding.prototype, - - sanitizeBoundValue: function(value) { - return Boolean(value); - }, - - postUpdateBinding: function() { - // Only the radio button that is getting checked gets an event. We - // therefore find all the associated radio buttons and update their - // CheckedBinding manually. - if (this.node.tagName === 'INPUT' && - this.node.type === 'radio') { - getAssociatedRadioButtons(this.node).forEach(function(radio) { - var checkedBinding = radio.bindings.checked; - if (checkedBinding) { - // Set the value directly to avoid an infinite call stack. - checkedBinding.value = false; - } - }); - } + function checkedPostEvent(input) { + // Only the radio button that is getting checked gets an event. We + // therefore find all the associated radio buttons and update their + // check binding manually. + if (input.tagName === 'INPUT' && + input.type === 'radio') { + getAssociatedRadioButtons(input).forEach(function(radio) { + var checkedBinding = radio.bindings.checked; + if (checkedBinding) { + // Set the value directly to avoid an infinite call stack. + checkedBinding.setValue(false); + } + }); } - }); + } - HTMLInputElement.prototype.bind = function(name, model, path) { + HTMLInputElement.prototype.bind = function(name, observable) { if (name !== 'value' && name !== 'checked') - return HTMLElement.prototype.bind.call(this, name, model, path); + return HTMLElement.prototype.bind.call(this, name, observable); - this.unbind(name); + unbind(this, name); this.removeAttribute(name); - return this.bindings[name] = name === 'value' ? - new InputBinding(this, 'value', model, path) : - new CheckedBinding(this, model, path); + + var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue; + var postEventFn = name == 'checked' ? checkedPostEvent : noop; + bindInputEvent(this, name, observable, postEventFn); + updateInput(this, name, + observable.open(inputBinding(this, name, sanitizeFn)), + sanitizeFn); + + return this.bindings[name] = observable; } - HTMLTextAreaElement.prototype.bind = function(name, model, path) { + HTMLTextAreaElement.prototype.bind = function(name, observable) { if (name !== 'value') - return HTMLElement.prototype.bind.call(this, name, model, path); + return HTMLElement.prototype.bind.call(this, name, observable); - this.unbind(name); - this.removeAttribute(name); - return this.bindings[name] = new InputBinding(this, name, model, path); - } + unbind(this, 'value'); + this.removeAttribute('value'); + + bindInputEvent(this, 'value', observable); + updateInput(this, 'value', + observable.open(inputBinding(this, 'value', sanitizeValue))); - function OptionValueBinding(element, model, path) { - InputBinding.call(this, element, 'value', model, path); + return this.bindings.value = observable; } - OptionValueBinding.prototype = createObject({ - __proto__: InputBinding.prototype, - - valueChanged: function(value) { - var select = this.node.parentNode instanceof HTMLSelectElement ? - this.node.parentNode : undefined; - var selectBinding; - var oldValue; - if (select && - select.bindings && - select.bindings.value instanceof SelectBinding) { - selectBinding = select.bindings.value; - oldValue = select.value; - } + function updateOption(option, value) { + var parentNode = option.parentNode;; + var select; + if (parentNode instanceof HTMLSelectElement && + parentNode.bindings && + parentNode.bindings.value) { + select = parentNode; + var selectBinding = select.bindings.value; + var oldValue = select.value; + } - InputBinding.prototype.valueChanged.call(this, value); - if (selectBinding && !selectBinding.closed && select.value !== oldValue) - selectBinding.nodeValueChanged(); + option.value = sanitizeValue(value);; + + if (select && select.value != oldValue) { + selectBinding.setValue(select.value); + selectBinding.reset(); + Platform.performMicrotaskCheckpoint(); + } + } + + function optionBinding(option) { + return function(value) { + updateOption(option, value); } - }); + } - HTMLOptionElement.prototype.bind = function(name, model, path) { + HTMLOptionElement.prototype.bind = function(name, observable) { if (name !== 'value') - return HTMLElement.prototype.bind.call(this, name, model, path); + return HTMLElement.prototype.bind.call(this, name, observable); - this.unbind(name); - this.removeAttribute(name); - return this.bindings[name] = new OptionValueBinding(this, model, path); - } + unbind(this, 'value'); + this.removeAttribute('value'); - function SelectBinding(element, property, model, path) { - InputBinding.call(this, element, property, model, path); + bindInputEvent(this, 'value', observable); + updateOption(this, observable.open(optionBinding(this))); + return this.bindings.value = observable; } - SelectBinding.prototype = createObject({ - __proto__: InputBinding.prototype, + function updateSelect(select, property, value, retries) { + select[property] = value; + if (!retries || select[property] == value) + return - valueChanged: function(value) { - this.node[this.property] = value; - if (this.node[this.property] == value) - return; + // The binding may wish to bind to an