How to update UI immediately in Angular - html

I have a dialog box where a user can copy a charts to different workspace card but when ever user click on selected workspace card to copy particular chart, the Chart count is not updated immediately on the selected workspace card.
The chart is copied but the chart count on the workspace card is not updated.
I can only see the chart count number change when I close the Copy Dialog and open it again.
I'll be really appreciated if I can get any suggestion or help on how I can update the chart count immediately on selected workspace card without having to close the dialog first.
Workspace Card.HTML
<div>
<span class="details ws-name" matLine>{{chartCount}} Chart{{chartCount !== 1 ? 's' : ''}}</span>
</div>
Workspace Card.TS
chartCount: number = 0;
ngOnInit(): void {
if(!this.workspace.chartCount && this.workspace.charts) {
this.chartCount = this.workspace.charts.length;
}
else {
this.chartCount = this.workspace.chartCount;
}
}
Select Workspace List.HTML
<div class="header">Copy Chart to a Workspace</div>
<mat-hint class="sub-header">Select one of the following workspaces. The chart will be automatically staged.</mat-hint>
<mat-progress-bar *ngIf="loading || copying" color="primary" mode="indeterminate"></mat-progress-bar>
<div class="list" *ngIf="!loading">
<mc-workspace-card *ngFor="let workspace of workspaces" [workspace]="workspace" [isCopyModal] = "true" (click)="copy(workspace)">
</mc-workspace-card>
</div>
Select Workspace List.TS
copy(workspace: Workspace) {
this.copying = true;
this.chartService.copyChartToWorkspace(this.chartGuid, workspace.guid).subscribe(newChart => {
this.copying = false;
this._snackBar.open(`Chart has been copied to ${workspace.name}`, 'Check it out!', { duration: 5000, panelClass: "snackbar-button" }).onAction().subscribe(() => {
this.dialogRef.close();
this.router.navigate(['workspace', workspace.guid], { state: { data: workspace.guid } });
})
})
}

Maybe you can use the async pipe like:
<p>{{ name | async}}</p>
Name can be anything!

I think why it is not immediately updating is because you are not using Two way binding. If I am correct, you want to update the charts count in the workspace card, then simply use:
<mc-workspace-card *ngFor="let workspace of workspaces;let index = index;" [(ngModel)]="workspaces[index]" [isCopyModal] = "true" (click)="copy(workspace)">
Note the use of [(ngModel)] here to implement it.

From what I can see you only ever update chartCount in the ngOnInit which happens exactly once (when you load the the component).
You would either need to update that charCount property in WorkspaceCard.ts or, probably easier, write a function to do the logic and use that.
WorkspaceCard.ts
getChartCount(): number {
return (!this.workspace.chartCount && this.workspace.charts) ? this.workspace.charts.length : this.workspace.chartCount;
}
WorkspaceCard.html
<div>
<span class="details ws-name" matLine>{{getChartCount()}} Chart{{getChartCount()!== 1 ? 's' : ''}}</span>
</div>

It seems Like your function to update the chart number in a workspace is written in the ngOnInit() and remember that anything within it will be called once when the component is loaded. What you can do is to write your codes in a function out of ngOnInt which you will be calling in ngOnInit(). It will help you to call it again after the copying of a chart so as to have your workspace updated.
updateChartNumber(){
if(!this.workspace.chartCount && this.workspace.charts) {
this.chartCount = this.workspace.charts.length;
}
else {
this.chartCount = this.workspace.chartCount;
}
}
Then call this function in OnInit()
OnInit(){
this.updateChartNumber()
}
After copying to workspace call the function again to update the workspace charts number
copy(workspace: Workspace) {
this.copying = true;
this.chartService.copyChartToWorkspace(this.chartGuid, workspace.guid).subscribe(newChart => {
this.copying = false;
this._snackBar.open(`Chart has been copied to ${workspace.name}`, 'Check it out!', { duration: 5000, panelClass: "snackbar-button" }).onAction().subscribe(() => {
this.dialogRef.close();
this.router.navigate(['workspace', workspace.guid], { state: { data: workspace.guid } });
})
})
this.updateChartNumber()
}
That's what I can help you, Let me know if you have questions
Way 2: You can also call the ngOnInit() whenever the function copy() is called.

Related

Aurelia binding to a function with parameter and forcing update

I have bound an isSelected(item) function to my button which will toggle the class to show if it is selected or not, however this function will not update on changes.
I can not use a computed property as it takes a parameter.
Is there any way to make this work with a bound function and getting it to update ideally with a #computedFrom decorator or the like?
Example here:
https://codesandbox.io/s/aurelia-typescript-sandbox-8oksr?fontsize=14
You will notice that person 2 is bound correctly via the function, but clicking the other items will not update the UI.
-
reason for doing this
The reason I want to do this is a result of a somewhat complex source array. Rather than the persons array I have given as an example here. My real domain is closer to a list of boxes with items that can have other boxes (potentially infinitely) where items can be selected at any level.
Update 2
So I ran across this issue on github. One of the answers indicated that passing an observed item into a method automatically makes the method observable. Currently, you're only passing person into isSelected(). But, person isn't being changed. I think you can accomplish what you're looking for by changing your isSelected() method like so (notice the change in the call to isSelected in the class binding of the button):
vm.ts
public isSelected(person, length){
return this.selections.find(item => item.name === person.name);
}
view.html
<button
class="btn-gradient mini ${isSelected(person, selections.length) ? 'green': 'red'}"
click.delegate="selectPerson(person)"
repeat.for="person of people">
${person.name}
</button>
Example: https://codesandbox.io/s/aurelia-typescript-sandbox-oelt7?fontsize=14
Original Post
I'm struggling with the same issue with trying to implement an isSelected() method for controlling a selected indicator class. I've looked into #computedFrom.
I may be wrong on this, but from what I've seen #computedFrom can only be used with un-paramaterized getters.
#computedFrom('firstName', 'lastName')
get fullName() { return `${firstName} ${lastName}`}
So the problem with what we're wanting to do is that we need to pass in an index or an item to our method -- which breaks our ability to use #computedFrom.
An alternative, which I don't really like ... but it does work, is to add an isSelected property to each of your person objects. Then your code would look something like this:
vm.ts
selectPerson(person){
person.isSelected = !person.isSelected; //De-selects if already selected
}
view.html
<button
class="btn-gradient mini ${person.isSelected ? 'green': 'red'}"
click.delegate="selectPerson(person)"
repeat.for="person of people">${person.name}</button>
(or, as was recently suggested to me, wrap your person object in a wrapper class)
public class SelectableWrapper {
constructor(public person : Person, public isSelected : boolean){}
}
Update 1
To address the issue of displaying the list of selected items (as well as "coloring" the selected items), you could do the following (in addition to what I've already shown):
vm.ts
//Remove selections property and add it as a getter
get selections(){
return this.people.filter(p => p.isSelected);
}
view.html
<div repeat.for = "person of selections">
${person.name}
</div>
Example here: https://codesandbox.io/s/aurelia-typescript-sandbox-u92nk?fontsize=14
multi select utilizing signaler
import "./app.css";
import {BindingSignaler} from 'aurelia-templating-resources';
import { inject } from "aurelia-framework";
#inject(BindingSignaler)
export class App {
message = "Hello World!";
message2 = "Hello World2!";
people = [{ name: "Person 1" }, { name: "Person 2" }, { name: "Person 3" }];
selections = [];
constructor(private signaler: BindingSignaler) {
this.selections.push(this.people[1]);
}
selectPerson(person) {
this.selections.push(person);
this.signaler.signal('select-signal')
}
color(person) {
return this.selections.includes(person) ? 'green' : 'red';
}
}
<template>
<h1>People</h1>
<div repeat.for="person of selections">
${person.name}
</div>
<button
class="btn-gradient mini ${color(person) & signal:'select-signal' }"
click.delegate="selectPerson(person)"
repeat.for="person of people"
>
${person.name}
</button>
</template>
single select
add selected into class,
assign person in it on change
and then use selected === person as condition
import "./app.css";
export class App {
message = "Hello World!";
message2 = "Hello World2!";
people = [{ name: "Person 1" }, { name: "Person 2" }, { name: "Person 3" }];
selections = [];
// add selected
selected = null;
constructor() {
// use selectPerson
this.selectPerson(this.people[1]);
}
selectPerson(person) {
this.selections.push(person);
// assign person to selected
this.selected = person;
}
}
<template>
<h1>People</h1>
<div repeat.for="person of selections">
${person.name}
</div>
<!-- use `selected === person` as condition -->
<button
class="btn-gradient mini ${selected === person ? 'green': 'red'}"
click.delegate="selectPerson(person)"
repeat.for="person of people"
>
${person.name}
</button>
</template>

Angular 6 - How to stop infinite polling in subscribe()

So I want to show an icon based on whether or not the number of projects in my list is > 3. I am using this getProjects() function that I need to subscribe to in order to get the data. I am setting a boolean when I subscribe that checks the number of projects in the list, then in my HTML, I use a ngIf to show the icon based on the boolean. I am able to get it to show correctly, however, I think I am constantly polling in my subscribe, and setting this boolean over and over again because it is making my webpage run really slow.
I have already tried the take(1) method which doesnt seem to stop the subscription, as well as set it to a "this.variable" scope inside my component. I am currently using event emitters however that is not working either.
This is my code so far,
Function that I subscribe to (in a different component):
getProjects(): Observable<ProjectInterfaceWithId[]> {
const organizationId = localStorage.getItem('organizationId');
return this.firestoreService.collection('organizations').doc(organizationId)
.collection('projects').snapshotChanges()
.pipe(
map(actions => actions.map(a => {
const data = a.payload.doc.data() as ProjectInterface;
const id = a.payload.doc.id;
return {id, ...data} as ProjectInterfaceWithId;
})),
map(list => {
if (list.length !== 0) {
this.buildProjectLookup(list);
this.projects = list;
return list;
}
})
);
}
Function that i use to get the data and set the boolean:
#Input() toggle: boolean;
#Output() iconStatus = new EventEmitter();
displayIcon() {
this.projectService.getProjects()
.pipe(take(1))
.subscribe(
list => {
if(list.length >= 3){
this.toggle = true;
this.iconStatus.emit(this.toggle);
}
});
}
HTML:
<i *ngIf="displayIcon()" class="material-icons">list</i>
Is there any way for me to literally just check the list length once so I don't get caught in this subscription loop? Thank you in advance!
It looks like it could be happening due to the ngIf referring to the displayIcon() method.
Every time change detection runs within your component, this method will be called. If your component is using default change detection, this will be very often.
see https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/ for more
One way this could be fixed is by making the ngIf refer to a variable instead.
For example, you could set a projects$ observable using
this.projects$ = this.projectService.getProjects()
.pipe(
take(1),
tap(projects => this.iconStatus.emit(projects.length >= 3))
);
This observable should likely be instantiated in your ngOnInit() method.
Then in your template you can use
<i *ngIf="(projects$ | async)?.length >= 3" class="material-icons">list</i>

Is it possible to programmatically select a item in a <li *ngFor> list?

I have the following list to be showed:
<li *ngFor="let player of players" [class.selected]="player === selectedPlayer" (click)="onSelect(player)">
<a>
<span class="badge">{{player.victories}}</span>
<span class="badge2">{{player.matches}}</span>
{{player.name}}
</a>
</li>
I pass the selectedPlayer to a desendant component using a symple function:
onSelect(player: Player): void {
this.selectedPlayer = player;
}
So the selected item is used to detail the player in another component.
The problem is: if I refresh the list the previously selectedPlayer will be lost, but I want to stay selected.
I just needed to "reselect" the correspondent selectedPlayer on the new list. Ts, html or Angular does the rest here (enlighten me if I'm wrong), in a way that I don't need to access the list and programmatically set the new selected.
For that I've made the following function:
private reSelectPlayer() {
for (let p of this.players) {
if (this.haveSelected) {
if (p.id === this.selectedPlayer.id) {
this.selectedPlayer = p;
}
}
}
}
Which I call on the refresh function:
private refreshPlayersList() {
this.playerService.getPlayerList().subscribe(
players => {
this.players = players
this.reSelectPlayer();
});
}
Notice that haveSelected is initialized with a false value and is setted to true when the user select an item on the list.
First of all, it's normal to add the behavior you want, you can add a flag.
Add a flag to the object. For this case, I will create a new object:
class pObject{
Player player;
isSelected boolean:= false;
}
Modified the selected function:
onSelect(obj: pObject){
this.pObject = obj;
this.isSelected = ! this.isSelected ;
}
Update the display:
enter code here
...

Is there a way to trigger a select event after typing 3 char?

Im trying to trigger an event after the user types 3 character in the select input, basically ive got a service that loads all the options of the select ONLY if there are 3 chars on the textbox of the select.
How can I do that?
My html:
<fnd-extended-select label="Seleziona Insegna:" [autocomplete]="true" [(ngModel)]="filter.ensign" (click)="ensignValues($event)">
<fnd-option *ngFor="let p of ensignValue?.ensign" [value]="p.id">{{p.description}}</fnd-option>
</fnd-extended-select>
My component.ts:
ensignValues() {
this.accordiService.ensignValues().subscribe(
(res: EnsignFilter) => {
this.ensignValue = new EnsignFilter(res);
console.log(res);
},
errors => {
Utils.notifyErrors(errors, this.notificationsService);
});
}
So call that ensignValues method only if your input length is >= 3. For that just check the model change. But after you got results you'll need to filter them by text user typed for that you can create custom filter pipe.
<fnd-extended-select label="Seleziona Insegna:" [autocomplete]="true" [(ngModel)]="filter.ensign" (change)="changed($event)">
<fnd-option *ngFor="let p of ensignValue?.ensign" [value]="p.id">{{p.description}}</fnd-option>
</fnd-extended-select>
Then in component you can have methods like:
changed(event) {
if(this.filter.ensign.length >= 3) {
this.ensignValues();
}
}
It can be done :
customInput : Subject<string> = new Subject();
in your template :
(ngModelChange)='inputValueChanged($event)'
So now listent to the event :
inputValueChanged(event){
this.customInput.next(event);
}
You'll have to subscribe to your Subject in the below way :
this.customInput.debounceTime(300).distinctUntilChanged().subscribe(value =>{
//call your method if value.length >=3
});

Material Design - stepper how to remove/disable steps?

I'm using Material - Angular2 Stepper, and I have additional steps that I want to add/enable depending on what the user selects in the first step.
I tried the following:
- Load the additional forms into an array,
- then loop through it in the template with *ngFor
<mat-vertical-stepper linear>
<mat-step [stepControl]="firstForm" label="First">
<!-- Some form controls -->
</mat-step>
<mat-step *ngFor="let f of additionalForms" [stepControl]="f.form"
[label]="f.label">
<!-- Additional Steps -->
</mat-step>
</mat-vertical-stepper>
This works well for adding new steps, the problem is I can't remove them. If the user happened to come back to first form, and uncheck something, these additional steps wouldn't be required.
So trying something like: this.additionalForms = [] doesn't remove the steps. (until you click on one of the "removed" steps, then it throws an error: Cannot read property 'editable' of undefined, and only then, they're removed visually)
I also tried doing ChangeDetectorRef.detectChanges()
and tried wrapping into the NgZone.run()
but made no difference
Any solutions for this?
So I managed with this work-around:
https://github.com/angular/material2/issues/7700#issuecomment-336138411
1) Make a reference to the stepper:
<mat-vertical-stepper #stepper></mat-vertical-stepper>
2) Then, on the .ts side:
import { ViewChild } from '#angular/core';
import { MatVerticalStepper } from '#angular/material';
#ViewChild('stepper') stepper: MatVerticalStepper;
clearAdditionalForms(): void {
this.inventoryForms = [];
this.stepper._stateChanged(); // <- this : Marks the component to be change detected.
}
This is calling a private method which is probably a really bad idea, so if you have a better/correct solution, let me know, and I'll change the answer
A slightly more angular way, avoiding the private method way is to record what you need to do on the form control used by the step. So for instance let's say we have a step:
<mat-step [stepControl]="secondFormGroup">
<form [formGroup]="secondFormGroup">
<!-- your controls here -->
</form>
</mat-step>
Then define your form group:
this.secondFormGroup = this._formBuilder.group({
check: [false, Validators.requiredTrue]
});
We have now defined a pseudo element "check", that will be validated by the step.
Let's say we set something with a click function:
doClick(item) {
this.secondFormGroup.controls.check.setValue(item === 'thevalue');
}
Angular material will now do the rest, you will not be able to move past the step until item === thevalue.
Add *ngIf in each step
<mat-step *ngIf="*expression*"></mat-step>
Also, If you want to do not return the privously, you can use stepper's editable property as below
<mat-vertical-stepper linear>
<mat-step [stepControl]="firstForm" label="First" [editable]="false">
<!-- Some form controls -->
</mat-step>
<mat-step *ngFor="let f of additionalForms" [stepControl]="f.form"
[label]="f.label">
<!-- Additional Steps -->
</mat-step>
</mat-vertical-stepper>
based on https://material.angular.io/components/stepper/overview#editable-step
Angular Material 8.2.3
Best would be a [disabled] option, but incredibly they didn't add! So I tried all and I ended up a clean way to customize the step-headers:
To show/hide a step of course simply use *ngIf (what else?).
To disable steps dynamically based on user clicks / state of the store:
With great results: no hover background effect, cursor is normal, single step-header unclickable, still looks with full color: not opaque.
steps: Array<HTMLElement> = [];
subscriptions: Array<Subscription> = [];
ngAfterViewInit() {
// Needs continuous monitoring
this.subscriptions.push(
this.<observable>.pipe(
tap((data: Data) => {
// IMPORTANT: If you have an *ngIf on the steps,
// you have to sync the references of the HTML elements
this.syncHTMLSteps();
this.updateStepState(1, false); // Always disabled
if (data.isXXX) {
this.updateStepState(5, false);
} else if (data.isYYY) {
this.updateStepState(2, false);
this.updateStepState(5, true);
}
})
).subscribe());
}
ngOnDestroy() {
this.subscriptions.forEach((subscription) => {
if (subscription) {
subscription.unsubscribe();
}
});
}
/**
* Reads from the Dom the list of HTML elements for the steps.
*/
private syncHTMLSteps() {
this.steps = [];
let increment = 1;
let stepper: HTMLElement = document.querySelector('.mat-stepper-vertical');
if (!stepper) {
increment = 2; // 2, because Angular adds 2 elements for each horizontal step
stepper = document.querySelector('.mat-horizontal-stepper-header-container');
}
for (let i = 0; i < stepper.children.length; i += increment) {
this.steps.push(stepper.children[i] as HTMLElement);
}
}
/**
* Enable/Disable the click on the step header.
*
* #param step The step number (starts from 1)
* #param enabled The new state
*/
private updateStepState(step: number, enabled: boolean) {
// If you prefer to start using step 0, remove -1 here
this.steps[step - 1].style.pointerEvents = enabled ? '' : 'none';
}