I'm aiming to have a dynamically growing list of input fields - whenever I start typing in an input field, a new empty one should appear beneath it.
I expected something like this to work;
function Model() {
// Build a basic observable array
this.inputs = ko.observableArray();
// Whenever this changes, or on initial creation of this computed
// add a new empty field if the last one has any text in it.
ko.computed(function () {
var len = this.inputs().length;
// If there are no fields (initial run) or the last element is falsey...
if (!len || this.inputs()[len-1]()) {
// Create a new observable and add it to the array.
this.inputs.push(ko.observable(""));
}
}, this);
}
Below is some basic HTML to bind the model to;
<ul data-bind="foreach: inputs">
<li><input data-bind="textInput: $data" /></li>
</ul>
When I type into the text box that correctly appears (showing that this function does run on creation) the computed does not get invoked.
So what must I do to get the computed to reevaluate properly? Is there a better way to achieve a dynamically growing list that actually works in knockout?
Here is jsfiddle of the exact code I have here, to help in debugging this problem.
There are two reasons why the current implementation doesn't function as expected:
You're binding the input field to the $data context property, which according to the docs is the string supplied to the observable. Bind to the $rawData property to bind to the actual observable.
Computed dependency tracking adds each observable it encounters during any evaluation run. Pushing a new observable apparently doesn't add a dependency. Initializing the observable array to a single observable would be a solution to this. By doing this, the !len check can also be removed.
function Model() {
this.inputs = ko.observableArray([ko.observable("")]);
ko.computed(function() {
if (!!this.inputs()[this.inputs().length - 1]()) {
this.inputs.push(ko.observable(""));
}
}, this);
}
ko.applyBindings(new Model());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: inputs">
<li>
<input data-bind="textInput: $rawData" />
</li>
</ul>
Below is an alternative routine. It is recursively adding an observable. When adding an observable, a subscription is created to track its changes. When it changes to a non-empty value, the subscription is disposed (it is only needed once) and the routine is repeated.
function Model() {
this.inputs = ko.observableArray();
this.addItem = function() {
var newItem = ko.observable("");
this.inputs.push(newItem);
var sub = newItem.subscribe(function(newValue) {
if (!!newValue) {
sub.dispose();
this.addItem();
}
}, this);
}
this.addItem();
}
ko.applyBindings(new Model());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: inputs">
<li>
<input data-bind="textInput: $rawData" />
</li>
</ul>
Related
This is a follow-up question to this one:
As explained in the above-linked answer:
When you provide an expression for a binding value rather than just a
reference to an observable, KO effectively wraps that expression in a
computed when applying the bindings.
Thus, I expected that when providing the changeCity as a binding expression (it is a function and not an observable), then changing the value on the input box would fire the changeCity function.
However, as you can see on the first snippet, it doesn't (Nor when binding it as changeCity()), but If changeCity is declared as a ko.computed, it does fire - see the second snippet.
Does it mean that a bounded function and a bounded computed are not completely the same with regard to dependency tracking?
First snippet - bounded function:
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.changeCity = function () {
if (self.country() == "England") {
self.city("London");
} else {
self.city("NYC");
}
}
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"> </h1>
<span data-bind="text: 'change the country, get out of focus, and nothing will happen...'"></span>
<br/>
<input data-bind="value: country" />
Second snippet - bounded computed:
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.changeCity = ko.computed(function () {
if (self.country() == "England") {
self.city("London");
} else {
self.city("NYC")
}
});
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"> </h1>
<span data-bind="text: 'change the country, get out of focus, and behold:'"> </span>
<br/>
<input data-bind="value: country" />
I take it that you're not just trying to solve a practical problem, but that you're mostly interested in the "theoretical difference" between passing a computed or a plain function to a binding. I'll try to explain the differences/similarities.
Let's start with an example
const someObs = ko.observable(10);
const someFn = () => someObs() + 1;
const someComp = ko.computed(someFn);
const dec = () => someObs(someObs() - 1);
ko.applyBindings({ someObs, someFn, someComp, dec });
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div>Obs. value: <span data-bind="text: someObs"></span></div>
<div>Computed created in binding: <span data-bind="text: someFn()"></span></div>
<div>Computed created in vm: <span data-bind="text: someComp"></span></div>
<button data-bind="click: dec">-1</button>
The example above shows that both someFn and someComp do the same thing. By referencing someFn() in a binding handler's value, you've essentially created a computed with a dependency to someObs.
Why this doesn't work in your first example
You never referenced your changeCity method in any knockout related code, which means there'll never be the chance to create a dependency. Of course, you can force one, but it's kind of weird:
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.changeCity = function () {
if (self.country() == "England") {
self.city("London");
} else {
self.city("NYC");
}
}
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"> </h1>
<span data-bind="html: 'change the country, get out of focus, and <strike>nothing</strike> <strong>something</strong> will happen...'"></span>
<br/>
<input data-bind="value: (changeCity(), country)" />
Why a regular computed does work
In your second example, you use a ko.computed. Upon instantiating a ko.computed, the passed function is evaluated once (immediately) and dependencies to all used observables are created.
If you were to change the ko.computed to a ko.pureComputed, you'll see your second example will also stop working. A pureComputed only evaluates once its return value is actually used and won't create dependencies until then.
The internals
Knockout wraps your binding's value in a function as a string. You can read more about this in an answer I wrote earlier.
We also know that any observable that is called inside a binding-handler's init method, creates a dependency that calls the binding's update method when a change happens.
So, in the example I gave, this is what happens:
The text binding is parsed
The function function() { return someFn(); } is passed as a value accessor to the text binding's init method.
The value accessor is called to initialize the text field
someObs is asked for its value and a dependency is created
The correct value is rendered to the DOM
Then, upon pressing the button and changing someObs:
someObs is changed, triggering the text binding's update method
The update method calls the valueAccessor, re-evaluating someObs and correctly updating its text.
Practical advice
To wrap up, some practical advice:
Use a ko.pureComputed when you create a new value out of one or more observable values. (your example)
self.city = ko.pureComputed(
() => self.country() === "england"
? "london"
: "nyc"
);
Use a subscribe if you want to create side effects based on an observable value changing. E.g.: a console.log of a new value or a reset of a timer.
Use a ko.computed when you want to create side effects based on a change in any of several observables.
the expected behavior in both snippets is that once the text in the input box is changed (and the focus is out), changeCity is fired (Happens on the 2nd, not on the 1st).
Ahhh, now I understand. You are describing what a subscription does.
First off, rid your mind of DOM events. The <input> field does not exist. All there is is your viewmodel. (*)
With this mind-set it's clear what to do: React to changes in your country property, via .subscribe(). The following does what you have in mind.
var handlerVM = function () {
var self = this;
self.city = ko.observable("London");
self.country = ko.observable("England");
self.country.subscribe(function (newValue) {
switch (newValue.toLowerCase()) {
case "england":
self.city("London");
break;
case "usa":
self.city("NYC");
break;
default:
self.city("(unknown)");
}
});
}
ko.applyBindings(new handlerVM());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<h3 data-bind="text: city"></h3>
<input data-bind="value: country" />
(*) Of course the <input> field still exists. But it helps to imagine the view (your HTML) as 100% dependent on your viewmodel. Knockout does all the viewmodel-view interaction for you. It takes care of displaying changes in the viewmodel data, and it takes care of feeding back user interactions into your viewmodel. All you should pay attention to is changes in your viewmodel.
Whenever you feel that you need to listen to a basic DOM event like "click", chances are that you are doing something wrong, i.e. chances are you are missing an observable, or a custom binding.
I have deferred updates enabled.
I have two components.
The first is a list, which is simply implemented as a div with a foreach data binding:
<div class="list-people" data-bind="foreach: { data: people, afterRender: afterRenderPeople }">
<!-- ko component: { name: "listitem-person", params: { person: $data } } --><!-- /ko -->
</div>
The second is the list item:
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
afterRender is called for each item in the foreach.
My afterRenderPerson function is simple enough:
public afterRenderPerson = (elements: any[], data: Person) => {
let top = $(element[0]).offset().top;
scrollTo(top);
};
The problem is that when afterRenderPerson is called the sub-component listitem-person hasn't yet been rendered.
Which means the element array passed to afterRenderPerson has 4 nodes:
A text node containing \n i.e. a new line.
A comment node containing <!-- ko component: { name: "listitem-person", params: { person: $data } } -->.
A comment node containing <!-- /ko -->.
A text node containing \n i.e. a new line.
None of these are suitable for getting the top pixel, and even if they were, the sub-component being rendered could affect the layout at that location changing the value of the pixel I'm trying to get.
Unfortunately it seems that the documentation for foreach doesn't take in to account the delayed nature of components.
If you need to run some further custom logic on the generated DOM elements, you can use any of the afterRender/afterAdd/beforeRemove/beforeMove/afterMove callbacks described below.
Note: These callbacks are only intended for triggering animations related to changes in a list.
There are two workarounds I've come across, neither of which are great, but that's why they're workarounds and not solutions!
user3297291 gave the suggestion in a comment of making a scrollTo binding that's placed on the child components.
Only workaround I can think of is to define a custom scrollTo binding and include it in the component template... Quite easy to implement, but still feels hacky and makes your inner component harder to reuse. You might also want to track this feature request – user3297291
This would simply be a custom binding that conditionally executes some code based on a value provided to it.
The bindings aren't called until the HTML has been inserted in to the DOM. That's not perfect, as later changes to the DOM could affect the position of the inserted HTML elements, but it should work for many situations.
I wasn't very keen on having to modify the child components though, I preferred a solution when remained encapsulated in the parent component.
The second workaround is to check to see if the child component HTML element exists in the DOM by it's ID. Since I don't know when they will come in to existence this has to be done in some sort of loop.
A while loop isn't suitable as it'll run the check far too often, in a "tight" loop, so instead setTimeout is used.
setTimeout is a horrid hack, and it makes me feel dirty to use it, but it does work for this situation.
private _scrollToOffset = -100;
private _detectScrollToDelayInMS = 200;
private _detectScrollToCountMax = 40;
private _detectScrollToCount = 0;
private _detectScrollTo = (scrollToContainerSelector: string, scrollToChildSelector: string) => {
//AJ: If we've tried too many times then give up.
if (this._detectScrollToCount >= this._detectScrollToCountMax)
return;
setTimeout(() => {
let foundElements = $(scrollToChildSelector);
if (foundElements.length > 0) {
//AJ: Scroll to it
$(scrollToContainerSelector).animate({ scrollTop: foundElements.offset().top + this._scrollToOffset });
//AJ: Give it a highlight
foundElements.addClass("highlight");
} else {
//AJ: Try again
this._detectScrollTo(scrollToContainerSelector, scrollToChildSelector);
}
}, this._detectScrollToDelayInMS);
this._detectScrollToCount++;
};
I made sure to put a limit on how long it can run for, so if something goes wrong it won't loop forever.
It should probably be noted that there is an "Ultimate" solution to this problem, and that's TKO, AKA Knockout 4.
But that's not "production ready" yet.
How to know when a component has finished updating DOM?
brianmhunt commented on Jun 20
knockout/tko (ko 4 candidate) latest master branch has this.
More specifically, the applyBindings family of functions now return a Promise that resolves when sub-children (including asynchronous ones) are bound.
The API isn't set or documented yet, but the bones have been set up.
This appears to work. I made a binding handler that runs a callback in its init (it uses tasks.schedule to allow a rendering cycle). Attaching it at the parent level does not get the children rendered in time, but attaching it to the virtual element does.
I designed it to work with a function whose signature is like afterRender. Because it runs for each of the elements, the callback function has to test that the data is for the first one of them.
ko.options.deferUpdates = true;
ko.bindingHandlers.notify = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make it asynchronous, to allow Knockout to render the child component
ko.tasks.schedule(() => {
const onMounted = valueAccessor().onMounted;
const data = valueAccessor().data;
const elements = [];
// Collect the real DOM nodes (ones with a tagName)
for(let child=ko.virtualElements.firstChild(element);
child;
child=ko.virtualElements.nextSibling(child)) {
if (child.tagName) { elements.push(child); }
}
onMounted(elements, data);
});
}
};
ko.virtualElements.allowedBindings.notify = true;
function ParentVM(params) {
this.people = params.people;
this.afterRenderPeople = (elements, data) => {
console.log("Elements:", elements.map(e => e.tagName));
if (data === this.people[0]) {
console.log("Scroll to", elements[0].outerHTML);
//let top = $(element[0]).offset().top;
//scrollTo(top);
}
};
}
ko.components.register('parent-component', {
viewModel: ParentVM,
template: {
element: 'parent-template'
}
});
function ChildVM(params) {
this.Name = params.person;
}
ko.components.register('listitem-person', {
viewModel: ChildVM,
template: {
element: 'child-template'
}
});
vm = {
names: ['One', 'Two', 'Three']
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<template id="parent-template">
<div class="list-people" data-bind="foreach: people">
<!-- ko component: { name: "listitem-person", params: { person: $data } }, notify: {onMounted: $parent.afterRenderPeople, data: $data} -->
<!-- /ko -->
</div>
</template>
<template id="child-template">
<div class="listitem-person">
<span data-bind="text: Name"></span>
</div>
</template>
<parent-component params="{ people: names }">
</parent-component>
I have some problems with nested view models in knockout using the mapping plugin. I'm able to recreate the problem, and I have created a fiddle for it here: Fiddle
I have stripped down the actual view and viewmodel, so don't expect the output to look nice, but it will get the message accros. This is my view:
<div data-bind="foreach: $root.selectedArmy().Units">
<div class="unitoverview">
<!-- ko foreach: UnitMembers-->
<div class="member">
<div>
<span class="name" data-bind="text: Name, click: $parent.RemoveTest"></span>
</div>
<div data-bind="foreach: test">
<span data-bind="text:$data, click: $parent.RemoveTest"></span>
</div>
<h1 data-bind="text: test2"></h1>
</div>
<!-- /ko -->
</div>
</div>
<span data-bind="click:AddUnit">CLICK TO ADD UNIT</span>
And this is my model:
var armymaker = armymaker || {};
var unitMapping = {
'UnitMembers': {
create: function (options) {
return new UnitMemberViewModel(options.data);
}
}
};
var UnitViewModel = function (unit) {
var self = this;
self.Name = ko.observable("unitname");
self.UnitDefinitionId = ko.observable(unit.Id);
ko.mapping.fromJS(unit, {}, self);
};
var UnitMemberViewModel = function (unitmemberdefinition) {
var self = this;
self.test = ko.observableArray([ko.observable('TEST'), ko.observable('TEST2')]);
self.test2 = ko.observable('TEST1');
self.RemoveTest = function () {
self.test.splice(0,1);
self.Name('BUGFACE');
self.test2('OKI!!');
};
ko.mapping.fromJS(unitmemberdefinition, {}, self);
};
var ViewModel = function () {
var self = this;
self.showLoader = ko.observable(false);
self.newArmy = ko.observable({});
self.unitToAdd = ko.observable(null);
self.selectedArmy = ko.observable({ Template: ko.observable(''), Units: ko.observableArray() });
self.AddUnit = function () {
var data = {'Name': 'My name', 'UnitMembers': [
{ 'Name': 'Unitname1' }
] };
self.unitToAdd(new UnitViewModel((ko.mapping.fromJS(data, unitMapping))));
self.selectedArmy().Units.push(self.unitToAdd());
self.unitToAdd(null);
};
};
armymaker.viewmodel = new ViewModel();
ko.applyBindings(armymaker.viewmodel);
What happens is the following:
I click the link CLICK TO ADD UNIT, and that created a UnitViewModel, and for each element in the UnitMember array it will use the UnitMemberViewModel because of the custom binder (unitMapper) that I am using.
This all seems to work fine. However in the innermost view model, I add some field to the datamodel. I have called them test that is an observableArray, and test2 that is an ordinary observable. I have also created a method called RemoveTest that is bound in the view to both the span that represent test2, and the span in the foreach that represent each element of the array test.
However when I invoke the method, the change to the observable is reflected in the view, but no changes to the observableArray is visible in the view. Check the fiddle for details.
Are there any reasons why changes to an obsArray will not be visible in the view, but changes to an ordinary observable will?
I have made some observations:
The click event on the observable does not work, only the click event on the elements on the observableArray.
It seems that self inside the click event does not match the actual viewmodel. If I go self.test.splice(0,1) nothing happens in the view, but self.test.splice only contains one element after that command. However if I traverse the base viewmodel (armymaker.viewmodel.Units()[0].UnitMembers()[0].test) is still contains two elements.
Calling splice on the traversed viewmodel (armymaker.viewmodel.Units()[0].UnitMembers()[0].test.splice(0,1)) removes the element from the view, so it seems in some way that the element referenced by self is not the same object as what is linked inside the view. But then, why does it work for the observable that is not an array?
There is probably a flaw with my model, but I can't see it so I would appreciate some help here.
You are basically "double mapping".
First with
self.unitToAdd(new UnitViewModel((ko.mapping.fromJS(data, unitMapping))));
and the second time inside the UnitViewModel:
ko.mapping.fromJS(unit, {}, self);
where the unit is already an ko.mapping created complete "UnitViewModel", this double mapping leads to all of your problems.
To fix it you just need to remove the first mapping:
self.unitToAdd(new UnitViewModel(data));
self.selectedArmy().Units.push(self.unitToAdd());
self.unitToAdd(null);
and use the mapping option inside the UnitViewModel:
var UnitViewModel = function (unit) {
var self = this;
self.Name = ko.observable("unitname");
self.UnitDefinitionId = ko.observable(unit.Id);
ko.mapping.fromJS(unit, unitMapping, self);
};
Demo JSFiddle.
SideNote to fix the "The click event on the observable does not work" problem you just need to remove the $parent:
<span class="name" data-bind="text: Name, click: RemoveTest"></span>
because you are already in the context of one UnitMemberViewModel.
I'm trying to use knockout to show in a span the value from an extend function.
Here is my HTML:
<input type="text" data-bind="value: clientName" />
<span data-bind="text: clientName.dummy"></span>
And my Javascript code:
Model = function() {
//MyExtension definition
ko.extenders.MyExtension = function(target, options) {
target.dummy = ko.observable('hello');
target.subscribe(function(newValue){
target.dummy = ko.observable(new Date().toString());
target.dummy.notifySubscribers(); //this shouldn't be necessary
});
return target;
};
this.clientName = ko.observable().extend({MyExtension: "some options here"} );
};
myModel = new Model()
ko.applyBindings(myModel);
Since "dummy" is itself an observable, the behaviour I was expecting is:
1) User changed input box text
2) knockout trigers the subscribe function (and this is hapening)
3) dummy value is updated with current time (happens to)
4) The span's text is updated with last dummy value (this is not happening)
Any idea why?
Thanks.
PS: Here is the full test: http://jsfiddle.net/AYLv8/5/
You are overriding your dummy property with a new completely new observable in the your subscribe function.
The correct implementation would be to update the dummy observable:
target.subscribe(function(newValue){
target.dummy(new Date().toString());
});
Demo JSFiddle.
In a basic table structure, I want to be able to display a set of data from an array of objects one at a time. Clicking on a button or something similar would display the next object in the array.
The trick is, I don't want to use the visible tag and just hide the extra data.
simply you can just specify property that indicate the current element you want to display and index of that element inside your observableArray .. i have made simple demo check it out.
<div id="persons"> <span data-bind="text: selectedPerson().name"></span>
<br/>
<button data-bind="click: showNext" id="btnShowNext">Show Next</button>
<br/>
</div>
//here is the JS code
function ViewModel() {
people = ko.observableArray([{
name: "Bungle"
}, {
name: "George"
}, {
name: "Zippy"
}]);
showNext = function (person) {
selectedIndex(selectedIndex() + 1);
};
selectedIndex = ko.observable(0);
selectedPerson = ko.computed(function () {
return people()[selectedIndex()];
});
}
ko.applyBindings(new ViewModel());
kindly check this jsfiddle
Create observable property for a single object, then when clicking next just set that property to other object and UI will be updated.