forked from knockout/knockout
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
375 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,7 @@ desktop.ini | |
.eprj | ||
perf/* | ||
*.orig | ||
|
||
*.bak | ||
.DS_Store | ||
npm-debug.log | ||
node_modules | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
}; | ||
|
||
})(); |