Angular - recalculate a variable on every change - javascript

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 ;
}

Related

challenge with router.navigate and localstorage

I have a code that uses router.navigate to forward the user to a place
abrirLista(categoria, id) {
localStorage.setItem('categName', JSON.stringify(categoria));
const slug = slugify(categoria);
this.router.navigate(['/lista', 'categoria', slug], { queryParams: { id } });
}
as you can see it saves data in localstorage
with this other code I retrieve the data in Angular's ngOnInit, and do the interpolation in my html
getCategName(){
if(localStorage.getItem('categName')) {
this.categName = JSON.parse(localStorage.getItem('categName'))
}
}
<li class="breadcrumb-item active" aria-current="page">{{categName}}</li>
the problem is that the abrirLista It's in a component in the header, so it doesn't update the page and doesn't enter ngOnInit to execute the function getCategName, so it can't update the variable this.categName
I made a button that updates, but the idea is that whenever I execute the function abrirLista he updated the variable, but I'm not able to see how to do it
teste(){
this.categName = JSON.parse(localStorage.getItem('categName'))
}
So what you are trying to do by using this browser API (localStorage) is to store data such that you persist it for the second component to consume it. However you do not have the right connection established between the components so they can communicate the change and update your view. Angular has its own mechanism for such purposes. You can use:
Notifer and Listener (Observer pattern) with RxJS Subjects.
For this paradigm you need to use a service (Singelton pattern) class to setup the communication between the notifier and the listener components.
#Input and #Output property and event binding to share data between components without a service

Refresh the parent component view in Angular 4

I have got 2 components, let's say, Component A is a list view and Component B is a details view. Each row from the list view is clickable and will redirect to Component B upon clicking.
Component B allows editing and saving the details. I have added a Back button to Component B to allow me to go back to the list view.
But the problem I am having is that I can't see the updated list view and have to manually refresh the browser, and then I can see the updated list there.
I have tried directly using window.location and it works but really I don't prefer this approach.
public back() {
window.location.assign('/listview');
}
I wonder if there's any better way to solve this problem?
Update:
public onSelected(model: MyModel) {
const detailsViewUrl = `/detailsview/${model.id}`;
this._router.navigateByUrl(detailsViewUrl );
}
You can just emit an #Output EventEmitter with a method on Parent that looks in the event for a change with a variable stored in the component like this:
#Output someOutput: EventEmitter = new Event Emitter<any>;
HTML:
<b-component (someOutput)=getOutput($event)></b-component>
AComponent:
getOut(event){
let output = event;
if(this.something != output){
this.ngOnDestroy(); // or something that you can use to make it
}
That should work as intended.
It sounds like this is an issue with Angular's change detection when changing the contents of an array. See here:
Angular 2: How to detect changes in an array? (#input property)
The solutions in this questions should work but an easy way I have used in the past to force changes in an array to be recognised by Angular is to reassign the array after making the changes:
myArray = [...myArray];
use following routing fuction on back button click
public back() {
this._router.navigateByUrl('/listview')
}
or
public back() {
this._router.navigate('/listview')
}
Try this,
Just called the list view again internally and hit db at same time so updated values will be displayed in the list view.
calling the route by using below:
this.router.navigate(['/listview']);
Seems like a change detection issue, there are some ways to manually trigger change detection like so:
Inject ChangeDetectorRef.
Call it when you go back like so:
public back() {
ChangeDetectorRef.detectChanges()
}
Refer to this: Triggering change detection manually in Angular

Reading OData contexts in onInit of controller

I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.

Why is the #Output EventEmitter required in this code example?

I'm currently reading about two way data binding in Angular 2 and reading this article.
https://blog.thoughtram.io/angular/2016/10/13/two-way-data-binding-in-angular-2.html
In this article, there is a child component with an #Input and #Output which allows a value inside the component to be bonded to a variable on its parent.
export class CustomCounterComponent {
counterValue = 0;
#Output() counterChange = new EventEmitter();
#Input()
get counter() {
return this.counterValue;
}
set counter(val) {
this.counterValue = val;
this.counterChange.emit(this.counterValue);
}
decrement() {
this.counter--;
}
increment() {
this.counter++;
}
}
parent HTML
<custom-counter [(counter)]="counterValue"></custom-counter>
<p><code>counterValue = {{counterValue}}</code></p>
So for me, I understand why the #Input is needed - however I don't understand how the #Output counterChange works because it's not even being subscribed by anything on the parent. However, it is necessary to have it there and also have it called counterChange in order to work.
The author of the article says
The next thing we need to do, is to introduce an #Output() event with
the same name, plus the Change suffix. We want to emit that event,
whenever the value of the counter property changes. Let’s add an
#Output() property and emit the latest value in the setter
interceptor:
Why do we need to have the same name plus change suffix? Is this some sort of Angular convention that I'm unaware of? I'm just trying to figure out which fundamental concept I've missed so I can understand how this is working.
I have a plunker of the code here if it'll help.
https://plnkr.co/edit/BubXFDQ59ipxEdnEHWiG?p=preview
The #Output() decorator enables the counterChange EventEmitter to be used in the Angular event syntax - (event name)="function()".
What stumbles you in this case is the ability of Angular to desugar the [(counter)] syntax (called 'banana in a box') to [counter]="..." (counterChange)="...". In other words, Angular will append *Change suffix to the property binding value when sees [(property name)] syntax.
I hope this answers the first question.
As to Why do we need to have the same name plus change suffix?, this is an Angular convention that helps utilizing the 'banana in a box' syntax.
Highly recommend this blog post that explains in details the Angular template syntax:
https://vsavkin.com/angular-2-template-syntax-5f2ee9f13c6a#4930

Can RefluxJS stores indicate which property has changed when they call trigger()?

I'm new to Flux as a whole, but I'm trying to get a grip on it by starting with Reflux, which seems a bit more opinionated and simpler to learn.
As I understand, Reflux stores have a trigger method which indicates the store's data has changed, and they pass the updated data into it. This data can then be set as a React component's state, (or as one of the state's properties) using the Reflux.connect mixin or similar methods.
But what if a store has multiple sets of data that need to be listened to separately? Let's say I'm modifying the TodoMVC RefluxJS example, and I wanted the TodoStore to also include a title property that indicated the name of the todo list (as well as the list, the list of TODO items). Lets say there is also a <Title> component that is listening for changes to the title property, and setting the title as its state when it does.
A call to this.trigger(title) would update the title component, but would also cause the todo component to try to use the title string as its state, so we need a way to indicate which data has been changed. Should these two properties (title and list) be separated into different stores? Or should all calls to trigger include a string that indicates the property: this.trigger("title", this.title) or this.trigger("todos", this.list). Or should all the data be combined into one object which is then picked by the listeners (e.g. using Reflux.connectFilter)?
this.trigger("todos", {
todos: this.list,
title: this.title
});
These last two examples introduce new data to the this.trigger() call, meaning that Reflux.connect can't be used any more, because connect takes the data returned from a store and directly sets the components state to it. Does this mean we have to use Reflux.listenTo(TodoStore,"onTodoChange"), and then filter out the trigger calls that aren't relevant to this component?
(1) Its very important stores broadcast data change event to the subscribed top level view components.(The so-called controller views, as explained in http://facebook.github.io/flux/docs/overview.html).
(2) The re-usable components, such as List, Title etc,etc. are self complete, these components should not understand store data structure. Use properties instead of setState for display data.
(3) Do you really want the store to hold different type of data, or does the data belong to a different store.
(4) If the store must hold different type of data, my preference is not to "filter" by action type. Update all the view components listening to the store for simplicity.

Categories