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.
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'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>
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 have a silly problem, where my only solution is a sloppy hack that is now giving me other problems.
See my fiddle,
or read the code here:
HTML:
<input id='1' value='input1' />
<template id='template1'>
<input id='2' value='input2' />
</template>
JS - Item View Declaration:
// Declare an ItemView, a simple input template.
var Input2 = Marionette.ItemView.extend({
template: '#template1',
onRender: function () {
console.log('hi');
},
ui: { input2: '#2' },
onRender: function () {
var self = this;
// Despite not being in the DOM yet, you can reference
// the input, through the 'this' command, as the
// input is a logical child of the ItemView.
this.ui.input2.val('this works');
// However, you can not call focus(), as it
// must be part of the DOM.
this.ui.input2.focus();
// So, I have had to resort to this hack, which
// TOTALLY SUCKS.
setTimeout(function(){
self.ui.input2.focus();
self.ui.input2.val('Now it focused. Dammit');
}, 1000)
},
})
JS - Controller
// To start, we focus input 1. This works.
$('#1').focus();
// Now, we make input 2.
var input2 = new Input2();
// Now we 1. render, (2. onRender is called), 3. append it to the DOM.
$(document.body).append(input2.render().el);
As one can see above, my problem is that I can not make a View call focus on itself after it is rendered (onRender), as it has not yet been appended to the DOM. As far as I know, there is no other event called such as onAppend, that would let me detect when it has actually been appended to the DOM.
I don't want to call focus from outside of the ItemView. It has to be done from within for my purposes.
Any bright ideas?
UPDATE
Turns out that onShow() is called on all DOM appends in Marionette.js, be it CollectionView, CompositeView or Region, and it isn't in the documentation!
Thanks a million, lukaszfiszer.
The solution is to render your ItemView inside a Marionette.Region. This way an onShow method will be called on the view once it's inserted in the DOM.
Example:
HTML
<input id='1' value='input1' />
<div id="inputRegion"></div>
<template id='template1'>
<input id='2' value='input2' />
</template>
JS ItemView
(...)
onShow: function () {
this.ui.input2.val('this works');
this.ui.input2.focus();
},
(...)
JS Controller
$('#1').focus();
var inputRegion = new Backbone.Marionette.Region({
el: "#inputRegion"
});
var input2 = new Input2();
inputRegion.show(input2);
More information in Marionette docs: https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#region-events-and-callbacks
Well, I managed to solve it by extending Marionette.js, but if anyone else has a better idea that doesn't involve extending a library, I will GLADLY accept it and buy you a doughnut.
// After studying Marionette.js' annotated source code,
// I found these three functions are the only places
// where a view is appended after rendering. Extending
// these by adding an onAppend call to the end of
// each lets me focus and do other DOM manipulation in
// the ItemView or Region, once I am certain it is in
// the DOM.
_.extend(Marionette.CollectionView.prototype, {
appendHtml: function(collectionView, itemView, index){
collectionView.$el.append(itemView.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.CompositeView.prototype, {
appendHtml: function(cv, iv, index){
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
if (itemView.onAppend) { itemView.onAppend() }
},
});
_.extend(Marionette.Region.prototype, {
open: function(view){
this.$el.empty().append(view.el);
if (view.onAppend) { view.onAppend() }
},
});
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.