I have an angular page that uses an observable parameter from a service. However when this observable updates, the page is not updated to match.
I have tried storing the result of the observable in a flat value, or altering a boolean to adjust the UI, but none seem to have any effect.
Logging the observable confirms that it does update correctly, and on re-navigating to the page the new value is shown.
Other conditional UI updates correctly modify the page, only one below (*ngIf="(entries$ | async), else loading") is causing this issue.
component.ts
export class EncyclopediaHomeComponent implements OnInit {
entries$: Observable<EncyclopediaEntry[]>;
categories$: Observable<string[]>;
entry$: Observable<EncyclopediaEntry>;
entry: EncyclopediaEntry;
isEditing: boolean;
constructor(private route: ActivatedRoute, private encyService: EncyclopediaService) {
this.entries$ = encyService.entries$;
this.categories$ = encyService.categories$;
this.entries$.subscribe(es => {
console.log(es);
});
route.url.subscribe(url => this.isEditing = url.some(x => x.path == 'edit'));
this.entry$ = route.params.pipe(
switchMap(pars => pars.id ? encyService.getEntry(pars.id) : of(null)),
);
this.entry$.subscribe(entry => this.entry = entry);
}
ngOnInit(): void {
}
updateEntry(entry: EncyclopediaEntry) {
this.encyService.updateEntry(entry.id, entry);
}
}
component.html
<div class="encyclopedia-container">
<ng-container *ngIf="(entries$ | async), else loading">
<app-enc-list [entries]="entries$ | async"
[selectedId]="entry ? entry.id : null"></app-enc-list>
<ng-container *ngIf="entry">
<app-enc-info *ngIf="!isEditing, else editTemplate"
[entry]="entry$ | async"></app-enc-info>
<ng-template #editTemplate>
<app-enc-edit [entry]="entry$ | async" [categories]="categories$ | async"
(save)="updateEntry($event)"></app-enc-edit>
</ng-template>
</ng-container>
</ng-container>
<ng-template #loading>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
<br>
<p>Loading Encyclopedia...</p>
</ng-template>
</div>
edit:
service.ts
export class EncyclopediaService {
entriesSubject = new ReplaySubject<EncyclopediaEntry[]>();
entries$ = this.entriesSubject.asObservable();
private _entries: EncyclopediaEntry[];
constructor(private file: FileService) {
file.readFromFile(this.projectName+'Encyclopedia').subscribe((es: string) => {
this._entries = JSON.parse(es);
this.entriesSubject.next(this._entries);
this.entries$.subscribe(es => this.file.writeToFile(this.projectName+'Encyclopedia', JSON.stringify(es)));
});
}
.
.
.
}
It seems like the component does not see changes.
I do not know why because |async will do this job.
but to fix it you can use ChangeDetector:
constructor(
private route: ActivatedRoute,
private encyService: EncyclopediaService
private changeDetectorRef: ChangeDetectorRef,
) {
this.entries$ = encyService.entries$;
this.categories$ = encyService.categories$;
this.entries$.subscribe(es => {
// setTimeout need to run without troubles with ng changes detector
setTimeout(_=>{this.changeDetectorRef.detectChanges()},0);
...
});
or
you can use markforCheck like it described there.
Related
I created a subscription in my template to watch for changes to an object. The initial loading of the object displays the correct data for the property tags, when I add an item to the data it goes to a web server and returns a list of all the tags that are attached to the item (to keep the item in sync with the server). However, the newly added item isn't reflected on the page. I am not 100% sure why. I think it is because my of() statement but I am not sure. What I am seeing is that zip().pipe() never gets executed.
Do I need to use something other than of?
Note: I am trying to follow the declarative pattern to eliminate the usage of .subscribe()
Sub-Note: Once I get it working I plan on trying to remove the subscribe on this line this.server.file().subscribe
Stackblitz
export interface FileInfo {
tags: string[];
}
#Component({
selector: 'my-app',
template: `
<input #tag /><button (click)="addTag(tag.value)">Add Tag</button>
<div *ngIf="data$ | async as data">
<div *ngFor="let tag of data.tags">{{ tag }}</div>
</div>
`,
})
export class AppComponent {
data$ = new Observable<FileInfo>();
constructor(
// Used to mimic server responses
private readonly server: WebServer
) {}
ngOnInit() {
// I plan on removing this subscribe once I get a grasp on this
this.server.file().subscribe((img) => {
this.data$ = of(img);
});
}
addTag(newTag: string) {
const data$ = this.server.save(newTag);
this.data$.pipe(concatMap((i) => this.zip(data$)));
}
private zip(tags$: Observable<string[]>) {
return zip(this.data$, tags$).pipe(
tap((i) => console.log('zipping', i)),
map(([img, tags]) => ({ ...img, tags } as FileInfo))
);
}
}
It sounds like what you want to do is have a single observable source that emits the latest state of your object as new tags are added. Then you can simply subscribe to this single observable in the template using the async pipe.
In order to accomplish this, you can create a dedicated stream that represents the updated state of your file's tags.
Here's an example:
private initialFileState$ = this.service.getFile();
private addTag$ = new Subject<string>();
private updatedfileTags$ = this.addTag$.pipe(
concatMap(itemName => this.service.addTag(itemName))
);
public file$ = this.initialFileState$.pipe(
switchMap(file => this.updatedfileTags$.pipe(
startWith(file.tags),
map(tags => ({ ...file, tags }))
))
);
constructor(private service: FileService) { }
addTag(tagName: string) {
this.addTag$.next(itemName);
}
Here's a StackBlitz demo.
You're missusing the observable. After you subscribe with it, in the template with the async pipe, you should not update it's reference.
If you need to update the data, you must use a Subject.
export class AppComponent {
private readonly data = new BehaviorSubject<FileInfo>(null);
data$ = this.data.asObservable();
constructor(
// Used to mimic server responses
private readonly server: WebServer
) {}
ngOnInit() {
this.server.file().subscribe((result) => this.data.next(result));
}
addTag(newTag: string) {
this.server
.save(newTag)
.subscribe((tags) => this.data.next({ ...this.data.value, tags }));
}
}
Also, your service could be a lot simpler:
#Injectable({ providedIn: 'root' })
export class WebServer {
private readonly tags = ['dog', 'cat'];
file(): Observable<FileInfo> {
return of({ tags: this.tags });
}
save(tag: string) {
this.tags.push(tag);
return of(this.tags);
}
}
Here's the working code:
https://stackblitz.com/edit/angular-ivy-my3wlu?file=src/app/app.component.ts
Try completely converting webserver.service.ts to provide an observables of tags and FileInfo like this:
import { Injectable } from '#angular/core';
import { concat, Observable, of, Subject } from 'rxjs';
import { delay, map, shareReplay, tap } from 'rxjs/operators';
import { FileInfo } from './app.component'; // best practice is to move this to its own file, btw
#Injectable({ providedIn: 'root' })
export class WebServer {
private fakeServerTagArray = ['dog', 'cat'];
private readonly initialTags$ = of(this.fakeServerTagArray);
private readonly tagToSave$: Subject<string> = new Subject();
public readonly tags$: Observable<string[]> = concat(
this.initialTags$,
this.tagToSave$.pipe(
tap(this.fakeServerTagArray.push),
delay(100),
map(() => this.fakeServerTagArray),
shareReplay(1) // performant if more than one thing might listen, useless if only one thing listens
)
);
public readonly file$: Observable<FileInfo> = this.tags$.pipe(
map(tags => ({tags})),
shareReplay(1) // performant if more than one thing might listen, useless if only one thing listens
);
save(tag: string): void {
this.tagToSave$.next(tag);
}
}
and now your AppComponent can just be
#Component({
selector: 'my-app',
template: `
<input #tag /><button (click)="addTag(tag.value)">Add Tag</button>
<div *ngIf="server.file$ | async as data">
<div *ngFor="let tag of data.tags">{{ tag }}</div>
</div>
`,
})
export class AppComponent {
constructor(
private readonly server: WebServer;
) {}
addTag(newTag: string) {
this.server.save(newTag);
}
}
Caveat: If you ever call WebServer.save while WebServer.tags$ or downstream isn't subscribed to, nothing will happen. In your case, no big deal, because the | async in your template subscribes. But if you ever split it up so that saving a tag is in a different component, the service will need to be slightly modified to ensure that the "save new tag" server API call is still made.
I am trying to create a reusable form editor component that shows/hides a toolbar at the bottom of the form based on if the form is dirty. The toolbar has a save button and a reset form button. Both of the buttons on the toolbar need to be controlled based on the validity of the form. I used this tutorial here which is great... but I want to be able to reuse the component by passing in any form.
Here is my code (notice I am creating a FormGroup and passing it directly to ContentEditorComponent as an Input. My custom dirtyCheck operator is at the bottom.)
content-editor.component.ts
#Component({
selector: 'content-editor',
template: `
<mat-card class="card">
<ng-content></ng-content>
<div class="footer" *ngIf="isDirty$ | async">
<button type="submit" [disabled]="form.invalid$">
</div>
</mat-card>
`,
})
export class ContentEditorComponent implements OnInit, OnDestroy
{
#Input('formGroup') form: FormGroup;
isDirty$: Observable<boolean>;
source: Observable<any>;
ngOnInit(): void {
this.source = of(cloneDeep(this.form.value));
this.isDirty$ = this.form.valueChanges.pipe(
dirtyCheck(this.source)
);
}
initForm(): void {
this.source = of(cloneDeep(this.form.value));
this.isDirty$ = this.form.valueChanges.pipe(
takeUntil(this.unsubscribeAll),
dirtyCheck(this.source)
);
}
reset(): void {
this.form.patchValue(this.source);
this.initForm();
}
}
service-area-editor.component.html
<content-editor [formGroup]="form">
... my form
</content-editor>
dirty-check.ts
export function dirtyCheck<U>(source: Observable<U>) {
let subscription: Subscription;
let isDirty = false;
return function <T>(valueChanges: Observable<T>): Observable<boolean> {
const isDirty$ = combineLatest([
source,
valueChanges,
]).pipe(
debounceTime(300),
map(([a, b]) => {
console.log(a, b);
return isDirty = isEqual(a, b) === false;
}),
finalize(() => this.subscription.unsubscribe()),
startWith(false)
);
subscription = fromEvent(window, 'beforeunload').subscribe(event => {
isDirty && (event.returnValue = false);
});
return isDirty$;
};
}
How do I get the dirtyCheck operator to work correctly when the form can be anything? I cant supply the dirtyCheck operator with a source stream because the source stream comes from the parent component.
I'm very new to Angular, and I'm really struggling to find a concise answer to this problem. I have a Form Component Here:
(I'm excluding the directives and imports as they're not really relevant)
export class JournalFormComponent implements OnInit {
public entries: EntriesService;
constructor(entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
}
The EntriesService service just stores an array of entries:
export class Entry {
constructor (
public num: number,
public name: string,
public description: string,
public text: string
) { }
}
The Form Component template renders a <h2> and a <app-input> Component for each entry in the EntriesService, which works. That looks like this:
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}"></app-input>
</div>
Here's the <app-input> Input Component:
#Component({
selector: 'app-input',
template: `
<textarea #box
(keyup.enter)="update(box.value)"
(blur)="update(box.value)">
</textarea>
`
})
export class InputComponent {
private value = '';
update(value: string) {
this.value = value;
}
getValue () {
return this.value;
}
}
The InputComponent stores the user's text perfectly, but I don't know how to pass that data to the Form Component's EntriesService to update the Entry in order to Export it or Save it later. How is this done?
I think I'm phrasing this question well, but I'm not sure. If you need clarification I'll provide it.
Not sure if it matters, but I'm using Angular 9.1.11
There are many ways to update the data from one component to another.
component to component using service or subjects
parent~child component data exchange using Input() and Output() decorators. Or by using #ViweChild() interactions.
and many more
But please do check the angular docs https://angular.io/guide/component-interaction .
Use the below simple code, u might need to include modules like FormsModule. and import Input(), Output etc
#Component({
selector: 'app-journal-form',
template: `
<div *ngFor="let entry of entries.entries; let i=index">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" [entry]="entry" [arrayIndex]="i" (updateEntry)="updateEntry($event)" ></app-input>
</div>`
})
export class JournalFormComponent implements OnInit {
constructor(private entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
updateEntry(event){
console.log(event);
this.entries[event.arrayIndex] = event.entry;
}
}
#Component({
selector: 'app-input',
template: `
<textarea [(ngModel)]="name"
(keyup.enter)="update()"
(blur)="update()">
</textarea>
`
})
export class InputComponent {
#Input() entry: any;
#Input() arrayIndex: number;
#Output() updateEntry: EventEmitter<any> = new EventEmitter();
name:string;
constructor() {
console.log(entry);
this.name = entry.name;
}
update(){
this.entry.name = this.name;
this.updateEntry.emit({entry: this.entry, arrayIndex});
}
}
Output event will help in this situation.
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" (entryChange) = "entry.text = $event"></app-input>
</div>
app-input component
export class InputComponent {
private value = '';
#Output() entryChange = new EventEmitter<string>();
update(value: string) {
this.value = value;
this.entryChange.emit(value);
}
}
Instead of entry.text = $event you can also pass it to any save function, like saveEntry($event);
I am having trouble getting an observable that is nested within a switch case to resolve. Directly inside ngSwitch before the cases I can get the value for the observable. However once I am inside the switch case my value always returns null.
I have tried a few different things to resolve this with no progress. Perhaps I have a miss-understanding of how the async pipe works in angular templates?
<!--template-->
<ng-container [ngSwitch]="(selectedFilterConfig$ | async)?.filterType">
<p>{{(selectedFilterValueMap$ | async | json)}}</p> <!--This resolves.-->
<div *ngSwitchCase="'select'">
<p>List these...</p>
<p>{{(selectedFilterValueMap$ | async | json)}}</p> <!--This always comes back null.-->
<p *ngFor="let item of (selectedFilterValueMap$ | async)"> <!--So does this...-->
{{item.display}}</p>
</div>
<div *ngSwitchDefault>
<p>Not a select!</p>
</div>
</ng-container>
/* Imports */
interface FilterColumnSelectItem {
value: string;
display: string;
}
export interface FilterSelection {
filter: string;
value: string;
}
#Component({
selector: 'app-add-new-filters-dialog',
styleUrls: ['./add-new-filters-dialog.component.scss'],
templateUrl: './add-new-filters-dialog.component.html',
})
export class AddNewFiltersDialogComponent implements OnInit {
public newFilterForm = this.formBuilder.group({
filterSelection: [''],
filterValue: [''],
});
public filterSelectionFormValue$: Observable<string>;
public filterValueFormValue$: Observable<string>;
public tableColumnConfigs$: Observable<ColumnMeta[]>;
public filterSelectionFormControl: AbstractControl;
public filterValueFormControl: AbstractControl;
public selectedFilterConfig$: Observable<FilterConfig>;
public selectedFilterMeta$: Observable<ColumnMeta>;
public selectedFilterValueMap$: Observable<FilterColumnSelectItem[]>;
constructor(
public dialogRef: MatDialogRef<AddNewFiltersDialogComponent>,
private formBuilder: FormBuilder
) {}
public ngOnInit(): void {
this.filterSelectionFormControl = this.newFilterForm.get('filterSelection');
this.filterValueFormControl = this.newFilterForm.get('filterValue');
this.filterSelectionFormValue$ = this.filterSelectionFormControl.valueChanges.pipe(val => val);
this.filterValueFormValue$ = this.filterValueFormControl.valueChanges.pipe(val => val);
this.selectedFilterMeta$ = combineLatest(this.tableColumnConfigs$, this.filterSelectionFormValue$).pipe(
map(([metas, filterSelection]) =>
metas.find(meta => meta.columnName === filterSelection)));
this.selectedFilterConfig$ = this.selectedFilterMeta$.pipe(
map(meta => meta != null ? meta.filterConfig : null));
this.selectedFilterValueMap$ = this.selectedFilterConfig$.pipe(
map(config => !!config.filterSelectOptions ? config.filterSelectOptions : null))
.pipe(map(selectionItems => {
if (!selectionItems) {
return null;
}
return orderBy(selectionItems, 'priority')
.map(selectionItem => ({value: selectionItem.value, display: selectionItem.display}));
}));
}
/* Rest of component */
}
I want be able to get the results for selectedFilterValueMap$ after the switch case has passed.
I have a component and then another component which is using it, and data is being parsed through to each other by means of a tab changing, what I am trying to do is when the page is left but the back button is pressed that the tab is remembered, to be able to do it I need to understand the data flow, the functions are identical so It's difficult to determine what is going on!
For instance, they are both using #Input and when I press a tab, both functions console if I add one in?
the component:
<div class="EngagementNavigationSecondary-Items"
*ngIf="!selectedIndividual">
<span
*ngFor="let item of navList; let i = index"
class="EngagementNavigationSecondary-Item"
[ngClass]="{
'EngagementNavigationSecondary-Item--Active': selectedItem === i,
'EngagementNavigationSecondary-Item--One' : navList.length === 1,
'EngagementNavigationSecondary-Item--Two' : navList.length === 2,
'EngagementNavigationSecondary-Item--Three' : navList.length === 3
}"
(click)="clickTab(i)">
{{ item.title | translate }}
</span>
</div>
<div *ngIf="selectedIndividual">
<span class="EngagementNavigationSecondary-Item" (click)="goBack()">
<tl-icon
size="18"
name="chevron-left">
</tl-icon>
{{ 'Go back' | translate }}
</span>
</div>
the logic:
export class EngagementNavigationSecondaryComponent implements OnInit {
#HostBinding('class.EngagementNavigationSecondary') block = true;
#Input() navList: EngagementNavigationItem[];
#Input() selectedItem: number = 0;
#Input() selectedIndividual: string;
#Output() change: EventEmitter<number> = new EventEmitter<number>();
#Output() deselectIndividual: EventEmitter<boolean> = new EventEmitter<boolean>();
className: string;
ngOnInit() {
this.className = `EngagementNavigationSecondary-Item${this.navList.length}`;
}
goBack() {
this.deselectIndividual.emit(true);
}
clickTab($event: number) {
this.selectedItem = $event;
this.change.emit($event);
}
}
and now this component being used - within the side container component:
<tl-engagement-navigation-secondary
*ngIf="navList"
[navList]="navList"
[selectedItem]="selectedTab"
[selectedIndividual]="selectedIndividual"
(change)="tabChange($event)"
(deselectIndividual)="selectedIndividual = undefined">
</tl-engagement-navigation-secondary>
logic:
export class EngagementSideContainerComponent implements OnChanges, OnInit {
#Input() focus: EngagementGraphNode;
#Input() selectedTab: number;
#Output() change: EventEmitter<number> = new EventEmitter<number>();
public navList: EngagementNavigationItem[];
public selectedIndividual: EngagementUser;
constructor(
private urlService: UrlService,
private router: Router,
private mixPanelService: MixPanelService,
private engagementService: EngagementService
) { }
ngOnInit() {
this.navList = this.getNavList();
}
tabChange(event) {
this.change.emit(event);
}
as you can see they are basically identical, so when I click on a tab, is the original component being called and then this is parsing data to the side container component? I think its important to understand so I can actually create the solution.
thanks if you can help!