Template not updating when elements in array change - javascript

I have an interesting setup in my template that uses values in an array to calculate and display a number
{{moduleProps.totals | values | sum | currency:'USD':true:'1.0-0'}}
Here is the values pipe
import { Pipe, PipeTransform } from '#angular/core';
import { values } from 'lodash';
#Pipe({
name: 'values',
})
export class ValuesPipe implements PipeTransform {
transform(value: any, ...args) {
if(value){
return values(value);
}
}
}
And here is the sum pipe
import { Pipe, PipeTransform } from '#angular/core';
import { reduce } from 'lodash';
#Pipe({
name: 'sum',
})
export class SumPipe implements PipeTransform {
transform(value: any, ...args) {
if(value){
return reduce(value,(a,b)=>a+b,0);
}
}
}
The array I'm using for this looks something like this...
[donation_earnings: 868, donation_pledges: 10, raffle_purchase_earnings: 4, fixed_purchase_earnings: 6, bid_earnings: 38]
When I try to go update one of these numbers later on like this...
this.moduleProps.totals.donation_pledges += events[0].amount;
Where the events[0].amount should be a number added to whatever the current number of that object property is.
When I do this, however, the template is not updating. When I log out the object property after doing the math I see the number is changed but the template doesn't hear about it.
After I do this I am also calling
this.changeDetectorRef.detectChanges();
To help with change detection on another array of items being pushed and popped but this doesn't seem to also be picking up the changes to the values in the array.
I have also tried running this in a NgZone.run() but that wasn't updating the template either.
Is there some sort of caveat I don't understand yet about the way angular 2 handles array change detection?
EDIT:
Also when I output the individual object properties to the page I see their numbers changing, it's only the number that's passed through the pipes that isn't updating

Related

How to use currency pipe to get values upto 2 decimal digits in component?

I have a values like 54781.7622000 , 1123.11. I want this values in a format like $54781.76 , $1123.11.
//import currency pipe
import { CurrencyPipe } from '#angular/common'
//initialize in constructor
constructor(private cp: CurrencyPipe)
this.formatedOutputValue = this.cp.transform(parseInt(this.outputValue));
I have tried sum extra parameters after this value like.
this.formatedOutputValue = this.cp.transform(parseInt(this.outputValue),1.2-2);
But doesnt workout.
You are not passing all the needed parameters to the transform()
This is what the currency pipe transform() accepts in Angular (Source: Angular codebase)
transform(value: any, currencyCode?: string, display: 'code'|'symbol'|'symbol-narrow'|string|boolean = 'symbol', digitsInfo?: string, locale?: string)
You can fix your issue by passing the right data to the transform() like below.
this.formatedOutputValue = this.cp.transform(this.outputValue, 'USD', 'symbol', '1.2-2');
Here is a working example on StackBlitz. You can also see how to directly use the pipe in the template in this example.
you can do it using currency pipe only
this.cp.transform(this.data,'USD','symbol','1.2-2')
Stackblitz
You can create pipe like this:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'formatedOutputValue'
})
export class FormatedOutputValuePipe implements PipeTransform {
transform(value: any, args?: any): any {
return value.toFixed(2).replace(/[.,]00$/, "");
}
}
In your template:
<div>{{data.value | formatedOutputValue}}</div>
You'll want to use the Currency pipe in your template like this:
{{ outputValue | currency }}
The default decimal point is 2 if a currency code is not specified.
Angular has excellent documentation on how to properly use their CurrencyPipe.

Looping over an array returned by an imported function?

We have the async pipe to resolve Observables within template *ngFor expressions. Is there a pipe that can resolve / call a function that returns an array to be looped over.
For example if we have something like this:
<app-todo *ngFor="let todo of todosFunction | call"
Then angular would use the call pipe to resolve the todosFunction into the array of todos it returns.
Use Case
export enum VISIBILITY_FILTER {
SHOW_COMPLETED = 'Completed',
SHOW_ACTIVE = 'Active',
SHOW_ALL = 'All'
}
export function VISIBILITY_FILTER_VALUES():string[] {
return Object.keys(VISIBILITY_FILTER).map(k => VISIBILITY_FILTER[k]);
}
I'd like to be able to loop over the VISIBILITY_FILTER_VALUES directly simply y importing and using it (without declaring it as a property on the component). Is this possible?
Or perhaps it's possible to create a pipe that processes the enum FILTER_VALUES directory and creates the array out of it?
You can simply do
<app-todo *ngFor="let todo of todosFunction()"> and define todosFunction(), which returns an array, in your ts file.
There aren't any built in ones but writing one is pretty simple:
Pipe
#Pipe({
name: 'call'
})
export class CallPipe implements PipeTransform {
transform(value: any, args?: any): any {
if(typeof value === 'function') {
return value(args);
}
}
}
Component.ts
export class AppComponent {
data(times) {
return [1, 2, 3].map(x => x*times);
}
}
Component.html
<p *ngFor="let i of data | call: 3">{{i}}</p>
This is a list of built-in pipes if you wonder.
You can just call the function like this? , there is no need of another pipe to do the same.
<app-todo *ngFor="let todo of todosFunction()">
however it is recommended to prepare your array inside the component and bind it to variable instead of calling a function.
Call function in template is really bad as function will be executed on each Change Detection cycle.
Pipe in other hand is pure (by default) so it will be called only when input parameters was changed.
#Pipe({
name: 'apply',
})
export class ApplyPipe implements PipeTransform {
transform<T, U extends any[]>(fn: (...fnArgs: U) => T, ...args: U): T {
return fn(...args);
}
}

Angular2 Using Pipes in Component.js

I'm learning Angular2 and I want to format a number adding thousand comma separator. As far as I have read this can be done using Pipes, the thing is that I want to format the number programmatically in the js file not in html (doing like var | number).
First of all I've realized there is no NumberPipe standalone pipe that I can work with (correct me if I'm wrong) the most similar one is CurrencyPipe in #angular2/common. So I have something like this:
import { Component } from '#angular/core';
import { CurrencyPipe } from '#angular/common';
#Component({
templateUrl: 'test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent {
public myNumber = 1000;
constructor(private currencyPipe: CurrencyPipe) {
var formatted = this.currencyPipe().transform(this.myNumber, 'MXN', true); // Is this correct?
}
}
But it throws me the following error:
Unhandled Promise rejection: No provider for CurrencyPipe! ; Zone: angular ;...
What am I doing wrong?
Thanks in advance.
Regards
First thing: you need to declare your pipe - add it to the NgModule declarations section:
declarations: [CurrencyPipe]
Second thing: pipes are not injectables, so you can't take its instance by using Angular dependency injection system. You need to create new instance of this pipe manually, like:
var formatted = (new CurrencyPipe()).transform(this.myNumber, 'MXN', true);
This actually works in an #Injectable display utility service with even less fuss than the previous answer involving modules. I imported my data model (below) and the pipe, then simply added the function. So, if you can't use the pipe directly in markup, use this trick!
export interface MoneyDTO extends SerializableDTO, JsonModelObjectDTO {
value?: string;
currency?: string;
}
import { CurrencyPipe } from '#angular/common';
formatMoney(money: MoneyDTO): string {
const cp: CurrencyPipe = new CurrencyPipe('en-US');
return money && money.value ? cp.transform(money.value, money.currency || 'USD', 'symbol') : null;
}

Return a pipe through another pipe

This is a follow-up question to Angular2 - assign pipe from a variable
What I'm looking for is a way to use a pipe based on a variable name.
I tried what Günter suggested and created a pipe that returns other pipes, but how would you return another pipe and make sure it's not rendered as text?
I have the following pipe:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'piper'
})
export class PiperPipe implements PipeTransform {
transform(value: any, args?: any): any {
return `{{${value} | ${args}}}`;
}
}
But when I feed it with a date string and "date" argument, like this:
<!-- with e.g. obj = { value: "2016-11-08T11:11:40.000Z", pipe: "date" } -->
<div>{{obj.value | obj.pipe}}</div>
It renders it as innerText:
<div>{{2016-11-08T11:11:40.000Z | date}}</div>
I tried [innerHTML] but no luck either.

How to update an array of observables using interval?

I’m trying to build a dashboard for charts using angular 2 and chart.js (via ngcharts). I’d like to have an array of charts that each update via an http request on a custom interval.
Right now I have three separate chart calls that push data to an array. I’m having trouble when it comes to the next iteration - if I push to the array again, I’ll end up with 3 more charts. I’d like the subscribers in the array to update with new data when the interval emits it.
I’m a little confused as to how to correctly structure the component/service/http relationship for my use case. I feel that I’m close but I’m definitely missing something. How can I get the interval/subscriber relationship to map to the view and update the existing charts on an interval?
Any help would be great!
Right now:
Service:
I’m implementing the interval here:
getSingleChartObsinterval(id: number, interval: number) : Observable<Graph> {
return Observable.interval(interval).flatMap(() => this.getSingleChartobs(id));
}
getSingleChartobs(id: number) : Observable<Graph> {
return this.jsonp.get(“api location”)
.map(response => this.extractJsonData(response, id) as Graph)
}
extractJsonData is just taking the response and manipulating it to work with the chart JS. It returns a Graph object that has properties that are easy to work with. I don’t have control of the API so I can’t reconfigure the response to include more than one graph.
The component:
import { Component } from '#angular/core';
import { ChartsModule } from 'ng2-charts/ng2-charts';
import { ChartService } from './chart.service';
import { Graph } from './graph';
import { OnInit } from '#angular/core';
import { Observable } from 'rxjs/Rx';
#Component({
selector: 'ab-chart',
styles: [`
.chart {
display: block;
}
`],
templateUrl: 'app/chart.component.html'
})
export class ChartComponent implements OnInit {
ngOnInit(): void {
console.log("Chart component init");
this.getSingleChart(3, 5000);
this.getSingleChart(5, 4000);
this.getSingleChart(6, 5000);
}
graph: Graph;
graphs: Graph[] = [];
constructor(
private chartService: ChartService
) {}
getSingleChart(id: number, interval: number): void {
this.chartService.getSingleChartObsinterval(id, interval)
.subscribe(x =>
this.graphs.push(x)
);
}
}
The view:
<div *ngFor="let graph of graphs" class="chart-container">
<base-chart
class="chart"
[datasets]="graph.datasets"
[labels]="graph.labels"
[options]="graph.options"
[chartType]="graph.type">
</base-chart>
</div>
Since each graph has its own id (I assume its unique) so I'd just change getSingleChart() method to update graphs object at specific key. Note I changed the graphs property from an array to an object:
graphs: {[key: number]: Graph} = {};
getSingleChart(id: number, interval: number): void {
this.chartService.getSingleChartObsinterval(id, interval)
.subscribe(x => this.graphs[id] = x);
}
get graphIds() {
return Object.keys(this.graphs);
}
Then in the template you need to iterate the array of keys (you can iterate the graphs object:
<div *ngFor="let id of graphIds" class="chart-container">
<base-chart
class="chart"
[datasets]="graphs[id].datasets"
[labels]="graphs[id].labels"
[options]="graphs[id].options"
[chartType]="graphs[id].type">
</base-chart>
</div>
Do you have a limited amount of charts? If you always have three you could leverage the combineLatest operator (if you have more you would have to use some form of recursion I guess).
In your component you could do the following:
this.graphs = this.getSingleChart(3, 5000).combineLatest(
this.getSingleChart(5, 4000),
this.getSingleChart(6, 5000),
(val1, val2, val3) => return [val1, val2, val3])
//.subscribe((arrayOfVal) => console.log(arrayOfVal);
This will return a new array every time one of the charts gets updated. If chart 2 gets a new value, the function (third argument of the combineLatest) will be called with the old value of 1, the new value for 2 and the old value of three.
In your template you could just be using this:
<div *ngFor="let graph of graphs | async" ...>
CombineLatest: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/combinelatest.md

Categories