Permalink
Browse files

Add textInput binding.

  • Loading branch information...
1 parent 6ecec35 commit 5c932e2d92ca32ce5ee72aec7b63d92dd6ed7452 @mbest mbest committed Jun 12, 2014
View
@@ -11,7 +11,7 @@ desktop.ini
.eprj
perf/*
*.orig
-
+*.bak
.DS_Store
npm-debug.log
node_modules
@@ -38,6 +38,7 @@ knockoutDebugCallback([
'src/binding/defaultBindings/style.js',
'src/binding/defaultBindings/submit.js',
'src/binding/defaultBindings/text.js',
+ 'src/binding/defaultBindings/textInput.js',
'src/binding/defaultBindings/uniqueName.js',
'src/binding/defaultBindings/value.js',
'src/binding/defaultBindings/visible.js',
@@ -0,0 +1,225 @@
+describe('Binding: TextInput', function() {
+ beforeEach(jasmine.prepareTestNode);
+
+ it('Should assign the value to the node', function () {
+ testNode.innerHTML = "<input data-bind='textInput:123' />";
+ ko.applyBindings(null, testNode);
+ expect(testNode.childNodes[0].value).toEqual("123");
+ });
+
+ it('Should treat null values as empty strings', function () {
+ testNode.innerHTML = "<input data-bind='textInput:myProp' />";
+ ko.applyBindings({ myProp: ko.observable(0) }, testNode);
+ expect(testNode.childNodes[0].value).toEqual("0");
+ });
+
+ it('Should assign an empty string as value if the model value is null', function () {
+ testNode.innerHTML = "<input data-bind='textInput:(null)' />";
+ ko.applyBindings(null, testNode);
+ expect(testNode.childNodes[0].value).toEqual("");
+ });
+
+ it('Should assign an empty string as value if the model value is undefined', function () {
+ testNode.innerHTML = "<input data-bind='textInput:undefined' />";
+ ko.applyBindings(null, testNode);
+ expect(testNode.childNodes[0].value).toEqual("");
+ });
+
+ it('For observable values, should unwrap the value and update on change', function () {
+ var myobservable = new ko.observable(123);
+ testNode.innerHTML = "<input data-bind='textInput:someProp' />";
+ ko.applyBindings({ someProp: myobservable }, testNode);
+ expect(testNode.childNodes[0].value).toEqual("123");
+ myobservable(456);
+ expect(testNode.childNodes[0].value).toEqual("456");
+ });
+
+ it('For observable values, should update on change if new value is \'strictly\' different from previous value', function() {
+ var myobservable = new ko.observable("+123");
+ testNode.innerHTML = "<input data-bind='textInput:someProp' />";
+ ko.applyBindings({ someProp: myobservable }, testNode);
+ expect(testNode.childNodes[0].value).toEqual("+123");
+ myobservable(123);
+ expect(testNode.childNodes[0].value).toEqual("123");
+ });
+
+ it('For writeable observable values, should catch the node\'s onchange and write values back to the observable', function () {
+ var myobservable = new ko.observable(123);
+ testNode.innerHTML = "<input data-bind='textInput:someProp' />";
+ ko.applyBindings({ someProp: myobservable }, testNode);
+ testNode.childNodes[0].value = "some user-entered value";
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(myobservable()).toEqual("some user-entered value");
+ });
+
+ it('For writeable observable values, when model rejects change, update view to match', function () {
+ var validValue = ko.observable(123);
+ var isValid = ko.observable(true);
+ var valueForEditing = ko.computed({
+ read: validValue,
+ write: function(newValue) {
+ if (!isNaN(newValue)) {
+ isValid(true);
+ validValue(newValue);
+ } else {
+ isValid(false);
+ }
+ }
+ });
+
+ testNode.innerHTML = "<input data-bind='textInput: valueForEditing' />";
+ ko.applyBindings({ valueForEditing: valueForEditing}, testNode);
+
+ //set initial valid value
+ testNode.childNodes[0].value = "1234";
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(validValue()).toEqual("1234");
+ expect(isValid()).toEqual(true);
+ expect(testNode.childNodes[0].value).toEqual("1234");
+
+ //set to an invalid value
+ testNode.childNodes[0].value = "1234a";
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(validValue()).toEqual("1234");
+ expect(isValid()).toEqual(false);
+ expect(testNode.childNodes[0].value).toEqual("1234a");
+
+ //set to a valid value where the current value of the writeable computed is the same as the written value
+ testNode.childNodes[0].value = "1234";
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(validValue()).toEqual("1234");
+ expect(isValid()).toEqual(true);
+ expect(testNode.childNodes[0].value).toEqual("1234");
+ });
+
+ it('Should ignore node changes when bound to a read-only observable', function() {
+ var computedValue = ko.computed(function() { return 'zzz' });
+ var vm = { prop: computedValue };
+
+ testNode.innerHTML = "<input data-bind='textInput: prop' />";
+ ko.applyBindings(vm, testNode);
+ expect(testNode.childNodes[0].value).toEqual("zzz");
+
+ // Change the input value and trigger change event; verify that the view model wasn't changed
+ testNode.childNodes[0].value = "yyy";
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(vm.prop).toEqual(computedValue);
+ expect(computedValue()).toEqual('zzz');
+ });
+
+ it('For non-observable property values, should catch the node\'s onchange and write values back to the property', function () {
+ var model = { modelProperty123: 456 };
+ testNode.innerHTML = "<input data-bind='textInput: modelProperty123' />";
+ ko.applyBindings(model, testNode);
+ expect(testNode.childNodes[0].value).toEqual("456");
+
+ testNode.childNodes[0].value = 789;
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(model.modelProperty123).toEqual("789");
+ });
+
+ it('Should support alias "textinput"', function () {
+ testNode.innerHTML = "<input data-bind='textinput:123' />";
+ ko.applyBindings(null, testNode);
+ expect(testNode.childNodes[0].value).toEqual("123");
+ });
+
+ it('Should write to non-observable property values using "textinput" alias', function () {
+ var model = { modelProperty123: 456 };
+ testNode.innerHTML = "<input data-bind='textinput: modelProperty123' />";
+ ko.applyBindings(model, testNode);
+ expect(testNode.childNodes[0].value).toEqual("456");
+
+ testNode.childNodes[0].value = 789;
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(model.modelProperty123).toEqual("789");
+ });
+
+ it('Should be able to read and write to a property of an object returned by a function', function () {
+ var mySetter = { set: 666 };
+ var model = {
+ getSetter: function () {
+ return mySetter;
+ }
+ };
+ testNode.innerHTML =
+ "<input data-bind='textInput: getSetter().set' />" +
+ "<input data-bind='textInput: getSetter()[\"set\"]' />" +
+ "<input data-bind=\"textInput: getSetter()['set']\" />";
+ ko.applyBindings(model, testNode);
+ expect(testNode.childNodes[0].value).toEqual('666');
+ expect(testNode.childNodes[1].value).toEqual('666');
+ expect(testNode.childNodes[2].value).toEqual('666');
+
+ // .property
+ testNode.childNodes[0].value = 667;
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(mySetter.set).toEqual('667');
+
+ // ["property"]
+ testNode.childNodes[1].value = 668;
+ ko.utils.triggerEvent(testNode.childNodes[1], "change");
+ expect(mySetter.set).toEqual('668');
+
+ // ['property']
+ testNode.childNodes[0].value = 669;
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(mySetter.set).toEqual('669');
+ });
+
+ it('Should be able to write to observable subproperties of an observable, even after the parent observable has changed', function () {
+ // This spec represents https://github.com/SteveSanderson/knockout/issues#issue/13
+ var originalSubproperty = ko.observable("original value");
+ var newSubproperty = ko.observable();
+ var model = { myprop: ko.observable({ subproperty : originalSubproperty }) };
+
+ // Set up a text box whose value is linked to the subproperty of the observable's current value
+ testNode.innerHTML = "<input data-bind='textInput: myprop().subproperty' />";
+ ko.applyBindings(model, testNode);
+ expect(testNode.childNodes[0].value).toEqual("original value");
+
+ model.myprop({ subproperty : newSubproperty }); // Note that myprop (and hence its subproperty) is changed *after* the bindings are applied
+ testNode.childNodes[0].value = "Some new value";
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+
+ // Verify that the change was written to the *new* subproperty, not the one referenced when the bindings were first established
+ expect(newSubproperty()).toEqual("Some new value");
+ expect(originalSubproperty()).toEqual("original value");
+ });
+
+ it('Should update observable on input event (on supported browsers) or propertychange event (on old IE)', function () {
+ var myobservable = new ko.observable(123);
+ testNode.innerHTML = "<input data-bind='textInput: someProp' />";
+ ko.applyBindings({ someProp: myobservable }, testNode);
+ expect(testNode.childNodes[0].value).toEqual("123");
+
+ testNode.childNodes[0].value = "some user-entered value"; // setting the value triggers the propertychange event on IE
+ if (!jasmine.ieVersion || jasmine.ieVersion >= 9) {
+ ko.utils.triggerEvent(testNode.childNodes[0], "input");
+ }
+ expect(myobservable()).toEqual("some user-entered value");
+ });
+
+ it('Should write only changed values to observable', function () {
+ var observable = ko.observable(), previousValue;
+ var valueForEditing = ko.computed({
+ read: observable,
+ write: function(newValue) {
+ expect(newValue).not.toEqual(previousValue);
+ previousValue = newValue;
+ observable(newValue);
+ }
+ });
+
+ testNode.innerHTML = "<input data-bind='textInput: valueForEditing' />";
+ ko.applyBindings({ valueForEditing: valueForEditing}, testNode);
+
+ testNode.childNodes[0].value = "1234";
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(valueForEditing()).toEqual("1234");
+
+ // trigger change event with the same value
+ ko.utils.triggerEvent(testNode.childNodes[0], "change");
+ expect(valueForEditing()).toEqual("1234");
+ });
+});
View
@@ -76,6 +76,7 @@
<script type="text/javascript" src="defaultBindings/styleBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/submitBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/textBehaviors.js"></script>
+ <script type="text/javascript" src="defaultBindings/textInputBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/uniqueNameBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/valueBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/visibleBehaviors.js"></script>
@@ -0,0 +1,147 @@
+(function () {
+
+if (window) {
+ // Detect Opera version because Opera 10 doesn't fully support the input event
+ var operaVersion = window.opera && window.opera.version && parseInt(window.opera.version());
+
+ var safariVersion = window.navigator.userAgent.match(/^(?:(?!chrome).)*version\/(.*) safari/i);
+ if (safariVersion) {
+ safariVersion = parseInt(safariVersion[1]);
+ }
+}
+
+// IE 8 and 9 have bugs that prevent the normal events from firing when the value changes.
+// But it does fires the selectionchange event on many of those, presumably because the
+// cursor is moving and that counts as the selection changing. The selectionchange event is
+// fired at the document level only and doesn't directly indicate which element changed. We
+// set up just one event handler for the document and use activeElement to determine which
+// element was changed.
+if (ko.utils.ieVersion < 10) {
+ var selectionChangeRegisteredName = ko.utils.domData.nextKey(),
+ selectionChangeHandlerName = ko.utils.domData.nextKey();
+ var selectionChangeHandler = function(event) {
+ var target = this.activeElement,
+ handler = target && ko.utils.domData.get(target, selectionChangeHandlerName);
+ if (handler) {
+ handler(event);
+ }
+ };
+ var registerForSelectionChangeEvent = function (element, handler) {
+ var ownerDoc = element.ownerDocument;
+ if (!ko.utils.domData.get(ownerDoc, selectionChangeRegisteredName)) {
+ ko.utils.domData.set(ownerDoc, selectionChangeRegisteredName, true);
+ ko.utils.registerEventHandler(ownerDoc, 'selectionchange', selectionChangeHandler);
+ }
+ ko.utils.domData.set(element, selectionChangeHandlerName, handler);
+ };
+}
+
+ko.bindingHandlers['textInput'] = {
+ 'init': function (element, valueAccessor, allBindings) {
+
+ var previousElementValue = element.value,
+ timeoutHandle,
+ elementValueBeforeEvent;
+
+ var updateModel = function () {
+ clearTimeout(timeoutHandle);
+ elementValueBeforeEvent = timeoutHandle = undefined;
+
+ var elementValue = element.value;
+ if (previousElementValue !== elementValue) {
+ previousElementValue = elementValue;
+ ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue);
+ }
+ };
+
+ var deferUpdateModel = function () {
+ if (!timeoutHandle) {
+ elementValueBeforeEvent = element.value;
+ timeoutHandle = setTimeout(updateModel, 4);
+ }
+ };
+
+ var updateView = function () {
+ var modelValue = ko.utils.unwrapObservable(valueAccessor());
+
+ if (modelValue === null || modelValue === undefined) {
+ modelValue = '';
+ }
+
+ if (elementValueBeforeEvent !== undefined && modelValue === elementValueBeforeEvent) {
+ setTimeout(updateView, 4);
+ return;
+ }
+
+ // Update the element only if the element and model are different. On some browsers, updating the value
+ // will move the cursor to the end of the input, which would be bad while the user is typing.
+ if (element.value !== modelValue) {
+ previousElementValue = modelValue; // Make sure we ignore events (propertychange) that result from updating the value
+
+ element.value = modelValue;
+ }
+ };
+
+ var onEvent = function (event, handler) {
+ ko.utils.registerEventHandler(element, event, handler);
+ };
+
+ if (ko.utils.ieVersion < 9) {
+ // Internet Explorer <=8 doesn't support the 'input' event, but does include 'propertychange' that fires whenever
+ // any property of an element changes. Unlike 'input', it also fires if a property is changed from JavaScript code,
+ // but that's an acceptable compromise for this binding.
+ onEvent('propertychange', function(event) {
+ if (event.propertyName === 'value') {
+ updateModel();
+ }
+ });
+
+ if (ko.utils.ieVersion == 8) {
+ // IE 8 has a bug where it fails to fire 'propertychange' on the first update following a value change from
+ // JavaScript code. To fix this, we bind to the following events also.
+ onEvent('keyup', updateModel); // A single keystoke
+ onEvent('keydown', updateModel); // The first character when a key is held down
+
+ registerForSelectionChangeEvent(element, updateModel); // 'selectionchange' covers cut, paste, drop, delete, etc.
+ onEvent('dragend', deferUpdateModel);
+ }
+ } else {
+ // All other supported browsers support the 'input' event, which fires whenver the content of element is changed
+ // through the user interface.
+ onEvent('input', updateModel);
+
+ if (ko.utils.ieVersion == 9) {
+ // Internet Explorer 9 doesn't fire the 'input' event when deleting text, including using
+ // the backspace, delete, or ctrl-x keys, clicking the 'x' to clear the input, dragging text
+ // out of the field, and cutting or deleting text using the context menu. 'selectionchange'
+ // can detect all of those except dragging text out of the field, for which we use 'dragend'.
+ registerForSelectionChangeEvent(element, updateModel);
+ onEvent('dragend', deferUpdateModel);
+ } else if (safariVersion < 5 && ko.utils.tagNameLower(element) === "textarea") {
+ // Safari <5 doesn't fire the 'input' event for <textarea> elements, but it does fire
+ // 'textInput'.
+ onEvent('textInput', updateModel);
+ } else if (operaVersion < 11) {
+ // Opera 10 doesn’t fire the 'input' event for cut, paste, undo & drop operations on <input>
+ // elements. We can try to catch some of those using 'keydown'.
+ onEvent('keydown', deferUpdateModel);
+ }
+ }
+
+ // Bind to the change event so that we can catch programmatic updates of the value that fire this event.
+ onEvent('change', updateModel);
+
+ ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element });
+ }
+};
+ko.expressionRewriting.twoWayBindings['textInput'] = true;
+
+// textinput is an alias textInput
+ko.bindingHandlers['textinput'] = {
+ // preprocess is the only way to set up a full alias
+ preprocess: function (value, name, addBinding) {
+ addBinding('textInput', value);
+ }
+};
+
+})();

0 comments on commit 5c932e2

Please sign in to comment.