Permalink
Browse files

Add textInput binding.

  • Loading branch information...
mbest committed Jun 12, 2014
1 parent 6ecec35 commit 5c932e2d92ca32ce5ee72aec7b63d92dd6ed7452
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.