When does aurelia's value binding propagate value changes? - javascript

I currently have a test where I have an input element with value.bind="someProperty" which works fine and correctly shows the value of that property in the input field.
However if I were to subscribe to the DOM input event on that field and when its invoked check the value of the property it is not the same as the DOM value, even if I were to do a delay of 100ms (via setTimeout(checkPropertyValue, 100);) the value is not always propagated to the model, so when exactly does the value get updated from the input field?
== Edit ==
Here is example code for the sort of thing that is occurring, a good point was raised in a passing conversation with someone that with view filters you may throttle value bindings so you cannot rely upon the model being updated straight away so you may have to listen out for model changes using ObserverLocator.
#customAttribute('some-atribute')
export class SomeAttribute
{
#bindable someObjectWhichUsesVM;
constructor(element) {
this.element = element;
}
attached() {
this.element.addEventListener("input", doSomethingWithModel);
}
doSomethingWithModel() {
this.someObjectWhichUsesVM.doSomething();
}
}
So the above code would check for input changes on an element (not the model bound) and then try to do something using data on the model, which wont be propagated.
I think I have already found the answer which is that I need to subscribe to the model for changes and basically make sure the change has occurred before I run the logic, but if there is a better way I am all ears.

Related

Angular - recalculate a variable on every change

I have a variable that stores the available cars at any moment. Is there a way to automatically re-evaluate this function on every change?
Just using this.carFactory.available in this case is not a solution, because this example I'm showing is simplified - the real calculation in my project is alot more complex.
calculateAvailableCars(){
this.carFactory.available.forEach(function(item){
this.availableCars.push(car.id);
}.bind(this));
}
How could I do this in Angular 2? In Angular JS there was the possibility to $watch a function.
I could of course manually call this function everytime something changes, but it would be nice not to have to call this function in every part of the application that can change the data.
Using template function reference with auto change detection
You can use this function output on template:
carOutput(): cars[] {
this.calculateAvailableCars()
return this.availableCars;
}
and use output on template:
<p>My car ratio is {{ carOutput() }} </p>
However this will trigger very aggressive change detection strategy on this variable. This solution is the simpliest one, but from engineering perspective rather worst: consumes tons of unnecessary function calls. One note, that hosting element must not be set to detect changes onPush.
Separate data model to parent component and pass as property to child
You can store car list display in separate component, and pass new car array as input property to this component:
<car-display [cars]="availableCars"></car-display>
Then you can set changeDetetcion policy in this component to onPush, and each time input property bind to availableCars will change, <car-display> will re-render.
If update relays on some host binding
If some external host action is triggering new cars calculation, then hostBinding may help:
#hostListener(`hover`) recalculateCars() {
this.calculateAvailableCars()
}
And finally, (because you describe your use case quite cryptically, without many details, thus I'm scratching all possible scenarios) if some external component action shall trigger re-calculation, you can hook to ngLifecycle ngOnChanges() if for example external input property change shall re-trigger cars calculation.
In other words and summing all that up, it depends who and from where triggers changes, that shall re-trigger available cars recalculation.
And very important, see an answer from #chiril.sarajiu, because what we are trying to work around here can be handled automatically by single observable. This requires additional setup (service, provide observable to components, e.c.t.) but it's worth.
--- EDIT ---
If each variable change shall retrigger data
As OP clarified, that changes are related with model bound to component. So another option with mentioned by #marvstar is using set, where each model variable change will retrigger fetching function:
modelSchangeSubject: Subject<Model> = new Subject<Model>();
ngOnInitt() {
this.modelSchangeSubject
.subscribe((v: Model) => {
this.calculateAvailableCars()
})
}
/* Rest of controller code */
set modelBounded(v: Model) {
this.modelSchangeSubject.next(v);
}
You need RxJS. What you do is you create a data service, which will store an Observable (in my case a BehaviorSubject, which is mostly the same, but in my case I start with a value).
export class DataService {
private dataStorage$ = new BehaviorSubject(null); //here is the data you start with
get getDataStorage() {
return this.dataStorage$.asObservable(); // so you won't be able to change it outside the service
}
set setDataStorage(data: any) {
this.dataStorage$.next(data);
}
}
Then you subscribe to this data changes everywhere you need to:
constructor(private dataService: DataService){}
ngOnInit() {
this.dataService.getDataStorage.subscribe((data) => this.calculateAvailableCars(data));
}
calculateAvailableCars(){
this.carFactory.available.forEach(function(item){
this.availableCars.push(car.id);
}.bind(this));
}
Read more about best practices of using RxJS in Angular, as there can be quite a bit of pitfalls and problems.
Try using setter and getter.
private _YourVariable:any;
public set YourVariable(value:any){
this._YourVariable = value;
//do your logik stuff here like. calculateAvailableCars
}
public get YourVariable():any{
return this._YourVariable ;
}

ko.toJS() gets 'undefined' for variable initialized to ""

I have a project in ASP.NET MVC 4 that uses knockoutjs to handle client-side stuff like keeping track of if a certain field has changed.
In the class declaration for my ViewModel, i have 2 observables, both initialized to "":
private _observables = {
query: ko.observable(""),
object: ko.observable("")
}
In the close function of a dialog box, I check both observables using the isDirty() method to find out if they've changed, and prompt the user about saving if changes are detected.
I've stepped through and figured out that object doesn't appear to be tracked correctly, despite the line above. Inside isDirty(), knockout pulls the current state like this:
target.isDirty = function () {
var currentState = ko.toJS(target);
....logic
returns dirty/notdirty
After the ko.toJS() call, the object field of currentState is always undefined, which causes it to fail the state check- because the initial state is properly recorded as "".
Even if I use self._observables.object("") to explicitly set object before the call to isDirty(), it's still undefined after the ko.toJS() call inside the 'dirty-checker'.
I thought the issue might be the binding in the view- it's bound to a hidden field that doesn't get user input, however as long as object is initialized, I don't see how that initial value is being lost/overwritten.

How to ignore Angular $watch event when I myself have caused it?

I have a custom UI element with link to ngModel:
scope:
{
ngModel : "="
}
There are two ways how the attached model might change:
it is changed from outside - in this case I want to update UI of my custom element
it is changed from inside - in this case I want to ignore the changes because my UI is already up-to-date
I have a watch:
$scope.$watch("ngModel", function(newValue){
// here I have complex logic to traverse newValue
// to see if it matches my current UI state
// if it matches, then I return
// if it does not match, then I sync my UI to the newValue
});
and I have a function which pushes current UI state to the model:
function pushSelectionToModel() {
// some code
$scope.ngModel = newState;
// some code
}
Everything works fine, but in cases when user is scrolling through my UI directive fast, ngModel watch is being triggered each time. My code to detect if newValue matches my current UI state is fairly complex, thus causing performance issues.
A natural solution seems to be somehow to ignore the $watch in case when I have just called pushSelectionToModel. I cannot just disable the watch in pushSelectionToModel before update and enable after that because the watch is executed later, after pushSelectionToModel has exited.
How do I tell Angular that for some particular model assignment operation I don't want to trigger some particular $watch?
Here is the relevant simplified Plunkr example
Essentially I want to prevent updateUi() from being called twice when I click Apply button. But the example is simplified, in reality I can't directly assign or compare innerName and ngModel values because in my actual code the values differ because of some transformations.
I've sovled a similar problem, by adding the following statement:
$scope.$watch('ngModel', function(newValue){
if($scope.innerName !== newValue){
// now execute code
http://plnkr.co/edit/r9sQax4VNqBraimQi9pz
but its more of an workaround...

Using this in callback for non-collection

I'm going through the leaderboard example right now, and I've finished it, but I'm not fully satisfied with my implementation of the add functionality.
To start with, we have
Template.player.events({
'click': function () {
Session.set("selected_player", this._id);
}
});
I find it a little bit confusing how this is associated with the player collection, but I imagine this has to do with the <template part. I am also able to do
Template.leaderboard.events({
'click input.delete': function () {
Players.remove(this._id);
}
...which does remove the player with the associated button entry.
Now for the actual question part: I have added this to the bottom of the leaderboard template:
<div>
Add player: (Name <input required name="name" id="name">)
(Score <input required name="score" id="score">)
<input class="add" type="button" value="Add">
</div>
This works fine, and I have Template.leaderboard.events['click input.delete'] working fine, but in order to get the values I use:
'click input.add': function () {
var name = document.getElementById('name').value,
score = document.getElementById('score').value;
It would make a lot of sense to me if I were able to use this in some way, or use the event to somehow get the values that correspond the inputs. This not only makes sense to me from a design standpoint, but it would also cover the case of having more than one of these kinds of forms displaying simultaneously.
So in short is there any way to get elements that are near the target element in the context of an event?
Every event handler is given two arguments: event and template. You can read more about these event handler arguments here: http://docs.meteor.com/#eventmaps
event.target is a reference to the DOM element that originated the event. You can then use something like jQuery's traversing functions to get an element nearby.
You could also set the input values as properties of the template instance. E.g. in the template's created handler, you create name and score properties:
Template.player.created = function() {
this.name = '';
this.score = '';
};
And then you update those values in the keyup events of your input textboxes:
'keyup #name': function(event, template) {
template.name = event.target.value;
},
'keyup #score': function(event, template) {
template.score = event.target.value;
}
This is the way the same way that widgets made for Ember update their values, as explained here: http://www.emberist.com/2012/04/12/two-way-binding-to-the-dom.html
Nice to see someone with so much street cred using Meteor! The best way to get the value is with event.currentTarget and to get stuff from the data contexts there is also another way which needs no DOM knowledge
Template.player.events({
'keypress #name':function(event,context) {
//Get the event sending this' value
console.log(event.currentTarget.value)
//Traverse the DOM on the template 'player' in this case
console.log(context.find('#score').value)
}
});
Basically the best way to get the value of the sender is to use event.currentTarget to access the DOM for that object sending the event.
The reason it's implemented this way is probably because any dom object can send an event and it won't necessarily always have a value field so a slight bit of knowledge of the DOM is required when handling the event maps but using event.currentTarget.value works for most form fields
Data contexts
Regarding the data contexts you should be able to use the data available in the templates from the helpers, e.g if theres a {{score}} & a {{name}} value in the template or a helper, which is passed in this case via the {{#each}} for each individual player.
this.name,
this.score;
Which is also the same as (I usually use context in my helper but template is another way of callng it i guess like in travellingprog's answer)
context.data.name,
context.data.score;
The this helps get data from the template's data context into event's so that one doesn't have to use hidden HTML attributes containing data, e.g with how the player is removed its a bit cleaner than storing the _id in the dom somewhere. That being said event.currentTarget or context.find(..) are the best way to get the data from a textfield.

data-win-bind issues: converter only runs once and unable to bind id of element

I have the following html that is bound to an object containing id and status. I want to translate status values into a specific color (hence the converter function convertStatus). I can see the converter work on the first binding, but if I change status in the binding list I do not see any UI update nor do I see convertStatus being subsequently called. My other issue is trying to bind the id property of the first span does not seem to work as expected (perhaps it is not possible to set this value via binding...)
HTML:
<span data-win-bind="id: id">person</span>
<span data-win-bind="textContent: status converter.convertStatus"></span>
Javascript (I have tried using to modify the status value):
// persons === WinJS.Binding.List
// updateStatus is a function that is called as a result of status changing in the system
function updateStatus(data) {
persons.forEach(function(value, index, array) {
if(value.id === data.id) {
value.status = data.status;
persons.notifyMutated(index);
}
}, this);
}
I have seen notifyMutated(index) work for values that are not using a converter.
Updating with github project
Public repo for sample (not-working) - this is a really basic app that has a listview with a set of default data and a function that is executed when the item is clicked. The function attempts to randomize one of the bound fields of the item and call notifyMutated(...) on the list to trigger a visual updated. Even with defining the WinJS.Binding.List({ binding: true }); I do not see updates unless I force it via notifyReload(), which produces a reload-flicker on the listview element.
To answer your two questions:
1) Why can't I set id through binding?
This is deliberately prevented. The WinJS binding system uses the ID to track the element that it's binding to (to avoid leaking DOM elements through dangling bindings). As such, it has to be able to control the id for bound templates.
2) Why isn't the converter firing more than once?
The Binding.List will tell the listview about changes in the contents of the list (items added, removed, or moved around) but it's the responsibility of the individual items to notify the listview about changes in their contents.
You need to have a data object that's bindable. There are a couple of options:
Call WinJS.Binding.as on the elements as you add them to the collection
Turn on binding mode on the Binding.List
The latter is probably easier. Basically, when you create your Binding.List, do this:
var list = new WinJS.Binding.List({binding: true});
That way the List will call binding.as on everything in the list, and things should start updating.
I've found that if I doing the following, I will see updates to the UI post-binding:
var list = new WinJS.Binding.List({binding: true});
var item = WinJS.Binding.as({
firstName: "Billy",
lastName: "Bob"
});
list.push(item);
Later in the application, you can change some values like so:
item.firstName = "Bobby";
item.lastName = "Joe";
...and you will see the changes in the UI
Here's a link on MSDN for more information:
MSDN - WinJS.Binding.as
Regarding setting the value of id.
I found that I was able to set the value of the name attribute, for a <button>.
I had been trying to set id, but that wouldn't work.
HTH
optimizeBindingReferences property
Determines whether or not binding should automatically set the ID of an element. This property should be set to true in apps that use Windows Library for JavaScript (WinJS) binding.
WinJS.Binding.optimizeBindingReferences = true;
source: http://msdn.microsoft.com/en-us/library/windows/apps/jj215606.aspx

Categories