There's a bit of story behind how we ran into this... Basically, we were calling trigger('change') on all of our form inputs to let other knockout observables know their value had been reset. But I really think it's a bug in Knockout. Asking here to see if anyone else has run into it (and StackOverflow is a much nicer interface than Knockout's google forums).
So if you have a hidden input who's value is data-bound to a computed observable and you call jQuery's trigger('change') on it, it wipes out the observable. If you drill into the code, you can see that on the view model object, the member object is replaced with a string of the last value on the computed observable before you triggered the change event.
JS fiddle showing the breakage in action: http://jsfiddle.net/2VvvE/1/
It uses console.log to output the object, so be warned if you try a browser without console (cough IE). You can see that the dependent observable is working fine until you hit the 'Break It' button, after which, the value stops updating and subsequent presses output the same thing. If you comment out the line with the trigger('change') on it and re-run the fiddle, you can see that it continues to work after each button press.
Apologies for not asking a real question - we already figured a work around where we only call trigger('change') on inputs that aren't hidden (pretty straightforward jquery selector in case anyone is curious):
$("#"+this.id+" form").each(function() {
$(this).validate().resetForm();
this.reset();
// Do some actions on all the inputs, then filter before calling the trigger
$(this).find('input,select').data('valid','true').filter(':not(:hidden)').trigger('change');
$(this).find('label,legend').removeClass('validated-error');
});
But I wanted a verdict: Knockout bug? Or am I doin' it wrong?
You should not bind a normal computed observable against a read/write binding like value. This is causing it to get overwritten in your model.
If you have to you can use a writeable computed observable. In this case you could even have a blank write function: http://jsfiddle.net/rniemeyer/2VvvE/2/
The actual answer though is that you really don't need to be triggering the change events on the fields. A better way to handle this is to do it from your view model. On any observable or computed observable you can call the valueHasMutated() function to notify all subscribers again with the latest value.
myObservable.valueHasMutated()
Related
I think I found a hard requirement to change behavior in a beforeChange handler based on the bindingContext of where a change is coming from. I already made some changes where a beforeChange handler gets to see the newValue along with the oldValue and where the handler can return boolean false to prevent the change from going forward.
The reason for this is the same objects may be bound to two (or more) different html nodes. The parent node may use different observables to wrap the same model object. But at some point, the child nodes are bound to properties through the same observables, sharing the same observable in more than one view element. When a change happens, I need to know from which of the two view elements the change originated.
I can save the bindingContext where the observable was made in the closure of my callbacks. But now I need the event to tell me what the bindingContext of the element was which just initiated the value change.
I can see the binding context somewhere in this domData thing, as follows: the valueUpdateHandler is called with the event object as argument it doesn't care about. But the event.target is our element that originated the change. I can see on that event target something like: "__ko__1655641582113" which I guess I am supposed to access with this ko.util.domData.
ko.utils.domData.get(arguments[0].target, "1__ko__1655641582113")
and lo and behold here I get an object with {alreadyBound: true, context: ko.bindingContext}
So I could force my way into this secret place and then get that context
ko.utils.domData.get($0, "1__ko__1655641582113").context.$parentContext.$rawData
and from there I could tell if I am in the element that should go forward with the change or not.
I am sure if anybody read this far that you'd be puzzled asking why in the hell I want to do that. But think about it, the observables are cool and all, but you sometimes need different behaviors based on where in your app the user is. In some areas they are just supposed to view, in others they can make (controlled) updates.
In other words, I know what I want is right and just, but I am wondering why it is so hard to get to what I want. For example, why the domData property is such a cryptic thing with a (timestamp) instead of a constant predictably named property. It's as if to tell me that I am making a big mistake even going there...
I'm using Backbone.js. In my view, I have a textarea whose keyup is bound to a function like this (but see edit below):
this.model.save({text: self.$('textarea').val()}, {patch: true});
In the view's initialize function, I bind the model's change event to the view's render function:
initialize: function() {
this.listenTo(this.model, 'change', _.bind(this.render, this));
},
Trouble is, when the user types in the textarea, the following sequence of events occurs:
The keyup event fires.
The keyup handler calls save on the model.
The call to save triggers the model's model's change event.
The view, listening for the model's change event, calls render.
The textarea is replaced in the DOM.
The textarea is no longer focused, and the text cursor position is lost.
What is the best practice for situations like this, where a texarea's keyup event needs to trigger a sync? Some options I have considered:
Don't bind change to render. Disadvantage: If the model data changes due to anything other than the user typing, the textarea doesn't automatically update.
Read and remember the cursor position at the beginning of render. Set the cursor position at the end of render. Disadvantage: Depends on cursor manipulation features for which browser support is spotty.
In the keyup handler, set a temporary property on the view telling it not to re-render. Unset it after the model has been saved. Disadvantage: Feels like spaghetti code, fights against the structure of Backbone.
Are there any options I'm not seeing? Do you recommend one of the options above?
Edit:
I didn't want to distract from the main point, but since it came up in one of the answers: I'm not binding directly to keyup, but intermediating it with _.debounce. Thus, the event handler only runs once the user stops typing, as defined by a certain amount of time elapsing since the last keyup.
First of all I'd like to discourage this as it seems like really strange behaviour to save your model on keyup. If there is a use-case which really necessitates this I'd suggest using the input event at the very least - otherwise you'll end up saving the model every time the user presses even an arrow key, shift, ctrl etc.
I think you'll also want to debounce the input events by 500ms or so you're not actually saving the model every single keystroke.
To address your comment in point 1:
Disadvantage: If the model data changes due to anything other than the
user typing, the textarea doesn't automatically update
You need to ask yourself the likelihood of this happening and how important it is that the view is rerendered if this was to happen.
Finally, if you decide that this is indeed likely and it is important that the view is rerendered, then you can try something like this
http://jsfiddle.net/nuewwdmr/2/
One of the important parts here is the mapping of model attribute names to the name field of your inputs. What I've done here follows the sequence of events you described above. The difference is that when the model changes, we inspect the changed attributes and update the value of the corresponding element in the template.
This works fine in a very simple situation, the happy path, where the user is typing in a "normal" way into the input. If the user, however, decides to go back to the start of the input and change a letter to capitalize it, for example, the cursor will jump to end of the string after the change event in the model occurs.
The behaviour you require here is really two-way data-binding which is by no means trivial, especially with Backbone given just how little functionality a Backbone View has.
My advice would be your point 1
Don't bind change to render
Edit
If you want to look further into model / view binding you could take a look at two libraries:
stickit
epoxy
I've used stickit before and it's...fine. Not great. It's ok for simple bindings, for example binding a "top-level" model attribute to an input element. Once you get into nested attributes you'll run into problems and you'll then have to look into something like Backbone Deep Model.
Like I said, Backbone's View doesn't offer very much. If you've got the time I'd suggest looking into using React components in place of Backbone Views, or even look at some of the interesting stuff that ampersand have to offer.
I see a lot of examples using React with backbone, there is however some things that are still somewhat unclear to me. In nearly all examples they show how you can get your component to listen to a model or collection and update the view accordingly, this seems pretty straightforward, you can use the Backbone Mixin or you can setup some event listeners in "componentDidMount()".
What is unclear to me is how to handle the other way, ie when a user writes in some input field, I then want to set this same value on my model, which ultimately is what i validate and then save on the server.
With simple forms this is also pretty straightforward, you can have a callback for the onChange event, example:
return <div><input type="text" onChange={this.setPrice} /></div>
All good, in the setPrice function I can now do something like:
this.props.myModel.set('price', e.target.value);
This works, but two things that immediately strike me:
The set method will be called on the model every single key event, since Reacts "onChange" actually executes on every key event, when you type in the textbox.
My second concern is, this works good for simple forms, however we have forms that have upwards 30-40 different input fields, having an onChange event on all of these input boxes, checkboxes and what have you seems counterproductive.
Right now, we have a databinding in our Backbone Views that simply sets whatever the user types on these input fields on the model, this does not seem to be the way togo in React though since what would be updated if you use something like ReactLink is the properties inside "state" on the Component, not properties directly on the model.
Is there a best practice here, or is this "marriage" between React and Backbone simply not meant to be? It would seem as if you would need to somehow map each input field to a specific property on the model. I am not sure if this is a good thing todo with React.
Thanks
You can call the setPrice method onBlur instead of onChange so that you will update the state when the user clicks or tabs out of the field.
This is more efficient for longer forms in my opinion as you are guaranteed that the user will tab or click to the next field.
I'm still running into the same problem, filters and functions inside ng-repeat being called all the damn time.
Example here, http://plnkr.co/edit/G8INkfGZxMgTvPAftJ91?p=preview, anytime you change something on a single row, someFilter filter is called 1000 times.
Apparently it's because any change on a child scope bubbles up to its parent, causing $digest to run, causing all filters to run(https://stackoverflow.com/a/15936362/301596). Is that right? How can I prevent it from happening in my particular case?
How can I make it run only on the item that has changed?
In my actual use case the filter is called even when the change is not even on the items of ng-repeat, it's so pointless and it is actually causing performance problems..
// edit cleared all the unnecessary stuff from the plunker
http://plnkr.co/edit/G8INkfGZxMgTvPAftJ91?p=preview
This is just how Angular's dirty checking works. If you have an array of 500 items and the array changes, the filter must be reapplied to the entire array. And now you're wondering "why twice"?
From another answer:
This is normal, angularjs uses a 'dirty-check' approach, so it need to call all the filters to see if exists any change. After this it detect that have a change on one variable(the one that you typed) and then it execute all filters again to detect if has other changes.
And the answer it references: How does data binding work in AngularJS?
Edit: If you're really noticing sluggishness (which I'm not on an older Core 2 Duo PC), there are probably a number of creative ways you can get around it depending on what your UI is going to be.
You could put the row into edit mode while the user is editing the data to isolate the changes, and sync the model back up when the user gets out of edit mode
You could only update the model onblur instead of onkeypress using a directive, like this: http://jsfiddle.net/langdonx/djtQR/1/
I've used the Dirty Flag Blog post here Knockmeout to implement such a flag in my model, but i can not get this to work properly. Somehow the flag is never set to true.
Additionaly i want my subscribe event to be triggered every time the dirty flag is set to true. (i'll to the reset manually).
Here's a fiddle that shows my issue.
Can someone point me in the right direction?
A couple of small things:
when you use span tags they should not be self-closing (so do <span></span>). This was preventing your final binding from being shown.
if you create your view model inside of an object literal, then this does not yet refer to the view model, so when you created your dirty flag it was not properly tracking your Filter object. If you want to do an object literal, then you would want to create your dirty flag afterwards.
the value binding when used with a select will populate your model value with a string. So, I changed your 1 to '1', otherwise it would be dirty immediately. There is a way to force it to be numeric using a writeable computed observable. Here is one technique.
Here is an updated sample: http://jsfiddle.net/rniemeyer/xw76d/4/