I have written a custom binding handler to bind my viewmodel data to a highcharts chart. This really has 2 parts, one binds the initial config required for highcharts, the second binds the series to the chart.
here is the bindingHandler code
ko.bindingHandlers.highchart = {
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var value = valueAccessor();
var valueUnwrapped = ko.unwrap(value);
console.log ('update',element.id,valueUnwrapped);
if(allBindings.get('series')){
var series = allBindings.get('series');
var seriesUnwrapped = ko.unwrap(series);
if(!$.isArray(seriesUnwrapped)){
seriesUnwrapped = [seriesUnwrapped];
}
console.log('seriesUnwrapped',element.id,seriesUnwrapped)
valueUnwrapped.series = seriesUnwrapped;
}
$(element).highcharts(valueUnwrapped);
}
}
Now I have 2 tests set up for this, the first works as expected. It binds a chart with multiple series, and when I add to the observable array bound to the series it updates the chart just once. Look at this fiddle and watch the console as you click the "add" button. The output you'll get is
update container Object { chart={...}}
seriesUnwrapped container [Object { name="Scenario0", color="red", data=[9]}, Object { name="Scenario1", color="green", data=[9]}]
Indicating that we've been through the above code only once.
Now check my second fiddle: http://jsfiddle.net/8j6e5/9/ . This is slightly different as the highcharts initial config is a computed observable, as is the series. When you click the "add" button on this one you'll see the binding is executed twice:
update container2 Object { chart={...}, xAxis={...}, series=[1]}
seriesUnwrapped container2 [Object { name="Scenario2", color="blue", data=[2]}]
update container2 Object { chart={...}, xAxis={...}}
seriesUnwrapped container2 [Object { name="Scenario2", color="blue", data=[2]}]
I'm guessing that using allBindings.get('series') within my highcharts binding handler I set up a dependency to it, and when both bindings change its executing the highcharts binding twice. My question is, is there any way to stop this, or write this functionality any other way as to not have this happen?
I'm not sure this will help you, as nemesv answers in the comments above seem to be very close to what you wanted to achieve, ie. stopping the double update.
However, I've spent a bit of time on it, so I'll show you what I came up with anyway and hope it helps.
Fiddle here
I didn't know about the peek() method that nemesv mentioned (great tip), so I looked into why it was updating twice based on the computeds etc.
I saw that your self.breakdownChart was accessing the currentScenario observable, and when I removed that as a test, the second update didn't occur.
So that got me thinking, why you needed that in there for the x axis setting.
So I added a new property to your scenario to return the current scenario name
self.name='Scenario' + self.number;
And then for the base scenario, changed this to "Base Scenario" to ensure that title appears correctly for just that series.
To ensure the legend/axis is correct, I added a new property to the chart object called baseSeriesName
self.breakdownChart = ko.computed(function(){
return {
baseSeriesTitle: baseScenario.name,
and that is set to the baseScenario's name.
Finally, to tie that all together in the BindingHandler, I update the xAxis in there:
//set the xAxis titles, only add the second title if different from the base
valueUnwrapped.xAxis={
categories: [valueUnwrapped.baseSeriesTitle, valueUnwrapped.baseSeriesTitle!=seriesUnwrapped[0].name ? seriesUnwrapped[0].name:'']
}
It's a bit of refactoring, but it achieves your goal; hope it helps.
Oh, I also added a chartType observable to the view model, and used that in the chart definition (breakdownChart computed), to test the double update wouldn't happen if the chart refreshed on a different observable and that it still initialised correctly - so the fiddle shows the chartType updating, without a double update.
You get two updates because Knockout updates computed observables immediately when their dependencies change, and your binding has two dependencies, each of which gets updated in turn.
One way to solve this is to use a technique to delay updates of the binding. An easy way to do so is to use the Deferred Updates plugin, as demonstrated here: http://jsfiddle.net/mbest/8j6e5/15/
Deferred Updates uses setTimeout to perform updates, which means that the update happens asynchronously. If you want it to be synchronous for a specific update, you can use ko.tasks.processImmediate:
ko.tasks.processImmediate(function() {
self.scenarios.push(newScenario);
self.currentScenario(newScenario);
});
Related
I am going through the Mithril tutorial and am having trouble understanding m.withAttr. The guide has the following line in the view layer:
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
I have two questions.
1) I understand that the first half, onclick: m.withAttr("checked", task.done) means, essentially:
'set task.done, using m.prop, to the value of the "checked" attribute'. But what is the purpose of the second half, checked: task.done()? It seems like it is simply repeating the first half.
2) As I went through the tutorial, I wanted to add the functionality of persisting my Todos to a persistence layer. I created a save function, so that I could refactor the line that I referenced above into something like:
m("input[type=checkbox]", { onclick: todo.vm.markAsDone.bind(todo.vm, task)})
And in my view-model, I had the function:
vm.markAsDone = function(todo) {
m.withAttr("checked", todo.done), checked: todo.done();
todo.save();
};
But this did not work; I get an Uncaught SyntaxError: Unexpected token : error. I think the problem is that the event is not being properly bound to the markAsDone function, so it doesn't understand the "checked" attribute; but I'm not sure how to fix this problem (if that even is the problem).
Thanks for any help.
Question 1
The second parameter of the m() function defines attributes on the HTML element, in this case an <input type=checkbox> will be decorated. (The exception is the special config field)
checked determines if the input checkbox is checked, so it is required to display the state of the task.
onclick is the event handler that will modify the state.
So the attributes do different things, therefore both of them are needed.
Question 2
Since markAsDone is passed a todo model, you don't have to do any m.withAttr call there. Simply modify the model, and let Mithril redraw the view. The redraw happens automatically if you call markAsDone through an event like onclick.
If you want more information about the redraw procedure, I summarized it in a previous SO question.
Edit: markAsDone will probably look like this:
vm.markAsDone = function(todo) {
todo.done(true);
todo.save();
};
I'm using bootstrap-switch together with the knockout binding handler referenced from this question shown below:
ko.bindingHandlers.bootstrapSwitchOn = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
$elem = $(element);
$elem.bootstrapSwitch();
// Set intial state
$elem.bootstrapSwitch('setState', ko.utils.unwrapObservable(valueAccessor()));
$elem.on('switch-change', function (e, data) {
// Update the model when changed.
valueAccessor()(data.value);
});
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var vStatus = $(element).bootstrapSwitch('status');
var vmStatus = ko.utils.unwrapObservable(valueAccessor());
if (vStatus != vmStatus) {
$(element).bootstrapSwitch('setState', vmStatus);
}
}
};
This seems to be working quite nicely and I've mocked up a fiddle to illustrate how I'm using it here:
http://jsfiddle.net/swervo/of0q42j0/5/
However, I have a few issues which I can't seem to solve in a satisfactory manner:
1) If I have an array of items in an ko.observable array I can put a click handler on all of them and have them call a function in the parent view model like this:
data-bind="click: $parent.clickHandler"
Which, when called, passes through the items own view model. This is really convenient for getting properties of the item that was clicked, eg., id. I've put a button in the fiddle above to illustrate how easy this is to do.
However, if I'm using the bootstrap-switch instead of a simple button the switch doesn't seem to know about it's parent and I can't find an elegant way of passing through the view model containing the switch to its parent - like you can with a button. I have tried giving each item in the array a reference to it's parent view model and this does work but creates a circular reference and thus doesn't seem like the correct approach.
2) In the application that I'm building the state of items in a list can be changed on a different clients - and the local state needs to update to reflect these remote clients. Equally the state can also be changed on the local client which is then propagated to other clients. My problem here is how to disambiguate between changes to state that have happened locally (ie., due to the user clicking on the switch), and changes that have happened remotely (ie., due to an update coming from the server). In my actual project I'm using knockout subscribe to listen for changes in the values linked to the switches like this:
viewModel.observableValue.subscribe(function(newValue) {
// test value on server and if it is different update
});
I want to avoid receiving an update from the server and then updating the server again (with the same state) when my switch changes to reflect the new state. At the moment I've fixed this by testing the server state (as implied in the code snippet above) before I send the update and if it is the same as the pending state update I discard it. (I've simulated a server update using a button in the referenced fiddle above).
Neither of my solutions to these problems feel elegant hence the question here.
Any help would be much appreciated.
I'm not sure what you mean by the 'the switch doesn't seem to know about it's parent'. Looking at http://knockoutjs.com/documentation/custom-bindings.html, I can see that init and update both have a 5th param, bindingContext that has the parent information, should you wish to access it.
Ahem, one of the projects we worked on the past had a toggle button that suffered from the same issue and it was fixed is a very simple way. For events that are generated locally, just attach a property to the object, like .local = true; and check for it in the update (or attach it in your REST handler) to distinguish local/vs REST. Don't forget to delete the property from the view model once done in update though.
It was always my understanding that .observes('someProperty') and .property('someProperty') worked exactly the same, except that the former is used for triggering function calls and the latter is used to keep object properties up to date.
But now I'm having a problem. My controller code looks like this:
_logChange: function(){
console.log('model array observer fired');
}.observes('model.#each'),
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each')
The observer and computed property both watch model.#each but for some reason, the observer fires on every model change and the property only updates TWICE before mysteriously going dead. statsData is calculated once on initial page load, and once on the first route transition, then after that, none of the transitions (with the changes in the underlying model they make) affect it.
What's going on here? Shouldn't they respond to change in the same way?
Note that I am using the statsData property in my template.
observers fire immediately, computed's fire as part of the run loop and scheduled in a debounced fashion. Currently all you're watching is that you add or remove an item to the collection, not whether or not a property on one of the items in the collection has changed. If you want to watch a particular property, you need to specify it.
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each.cost')
if you just want to watch the collection changing you should just use []
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.[]')
Thanks to the lovely folks on Ember IRC, I was able to figure it out. The problem was that I was passing statsData to a component, like this: {{common-statistics values=statsData}} and in the component, I had this function:
_validateValues: function(){
var values = this.get('values');
if(!values || !Ember.isArray(values) || values.length === 0)
{
this.set('values',[]);
}
}.on('willInsertElement')
which is, as you can see, setting values if it's not what the component is expecting. Unfortunately, this was affecting statsData on the controller as well, thanks to this JavaScript language feature. By setting statsData in the component, I was breaking the computed property on the controller.
So it was never a problem with Ember at all. I just failed to realize that object properties on Ember objects behave the same way they do on "regular JavaScript objects."
I've a view with knockout.js which has some textboxes and dropdowns.
known when the user changes a value i save the data with a $post
for this i created some computed propties like
self.subjectChanged ko.computed(function () {
var subject self.subject();
//save...
But this also triggers when the subject was loaded from database and set for first time.
What is the best practice for this ?
A similar problem is that i have a function getdata() which depends on two properties.
Now on load this method is raised twice (for each property)
What are best practices to handle this szenarios ?
One way of doing it is to load the page and bind the data as normal, and then use subscriptions to monitor changes to the observable you are interested in.
http://knockoutjs.com/documentation/observables.html#explicitly-subscribing-to-observables
viewModel.subject.subscribe(function(newValue) {
// code you want to run when the value changes...
});
for example http://jsfiddle.net/m8mb5/
This may not be best practice, but in the past I tied a loaded variable to the vm and when the data finished loading from the server I set it to true;
In my computeds I would surround the code that actually did the work in an if that checked the loaded. Computeds can be a little tricky though, you may need to reference the observables outside of the if to ensure they fire correctly.
com = ko.computed(function(){
if(loaded){
var subject = self.subject();
}
// reference observable outside of if to ensure the computed fires when the observable changes
self.subject();
});
I'm trying to push the object that populated a view into an array, but the reference is somehow getting lost. I've got an Ember view, with a defined eventManager:
FrontLine.NewProductButton = Em.View.extend({
tagName: 'button',
classNames: ['addtl_product',],
templateName: 'product-button',
eventManager: Ember.Object.create({
click: function(event, view) {
FrontLine.ProductsController.toggleProductToCustomer(event, view);
}
})
})
That view renders a bunch of buttons that are rendered with properties that come from objects in the ProductsController using the #each helper. That part works great. And when I click on any of those buttons, the click event is firing and doing whatever I ask, including successfully calling the handler function (toggleProductToCustomer) I've designated from my ProductsController:
FrontLine.ProductsController = Em.ArrayController.create({
content: [],
newProduct: function(productLiteral) {
this.pushObject(productLiteral);
},
toggleProductToCustomer: function(event, view){
FrontLine.CustomersController.currentCustomer.productSetAdditional.pushObject(view.context);
}
});
I'm trying to use that function to push the object whose properties populated that view into an array. Another place in my app (a simple search field), that works perfectly well, using pushObject(view.context). Here, however, all that gets pushed into the array is undefined. I tried using view.templateContext instead, but that doesn't work any better. When I try console.log-ing the button's view object from inside those functions, I get what I'd expect:
<(subclass of FrontLine.NewProductButton):ember623>
But either view.context or view.templateContext return undefined. How do I access the object I'm after, so I can add it to my array?
The simple answer is that it was one letter's difference:
view.content
or:
view.get('content')
provides the source object in that particular situation, rather than view.context.
(My only real challenge with Ember so far is that accessors for objects and properties vary so much from situation to situation, and there's no real documentation for that. Sometimes the object is at view.context, sometimes it's at view.content, sometimes _parentView.content, etc., etc. It would be awesome if there were a chart with the umpteen different syntaxes for accessing the same data, depending on which particular aperture you're reaching through to get it. I'm still discovering them...)