TL,DR; I have successfully passed the output of Async Pipe into a child component. But when the condition fails (filter => false), I want to pass in the previously emitted value from an Observable.
Detail;
Using Selectors, I subscribe to two slices of @ngrx/store:
export interface AppState {
data: string[];
pending: boolean; // Are we waiting for a HTTP response?
dirty: boolean; // Has the data been edited locally?
}
...although there are three slices above, this question involves only dirty and data.
The reducer has three cases:
AppActions.GET_NEW_DATA:- Set
pendingtotrueto update the UI. - Results in a HTTP request being made (via ngrx/effects).
- Set
AppActions.GET_NEW_DATA_SUCCESS:- Set
pendingtofalseto update the UI. - Set
dirtytofalsebecause no edits have been made.
- Set
AppActions.EDIT_DATA:- Update data provided in payload.
- Set
dirtytotruebecause an edit has been made.
The code:
export function appReducer(state: AppState = initialState,
action: AppActions.All
): AppState {
switch (action.type) {
case AppActions.GET_NEW_DATA:
// Action results in HTTP request.
return {
...state,
pending: true
};
case AppActions.GET_NEW_DATA_SUCCESS:
// HTTP response, update data!
return {
data: action.payload,
pending: false,
pristine: true,
};
case AppActions.EDIT_DATA:
return {
...state,
data: action.payload,
pristine: false
};
default: {
return state;
}
}
}
As mentioned earlier, using Selectors, I subscribe to two slices of @ngrx/store from within the smart component
@Component({
selector: 'app-smart',
templateUrl: './smart.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SmartComponent {
public data$: Observable<string[]>;
public dirty$: Observable<boolean>;
constructor(private readonly store: Store<AppState>) {
this.data$ = this.store.select(fromReducer.getData);
this.dirty$ = this.store.select(fromReducer.getDirty);
}
}
The smart component uses Async Pipe to detect when a new value is emitted from the Observable and mark the dumb component to be checked for changes.
<!-- From within <app-smart> -->
<app-dumb [data]="data$|async"></app-dumb>
This works as expected, however, I only want to be updated if dirty is false - i.e. when a new HTTP request is made and response received.
I don't want to redraw <app-dumb> when the data$ Observable emits a change from an editing. In summary, I require the following behavior:
When creating:
data$emits new value.dirty$isfalse.- Mark
<app-dumb>to be checked for changes.
When editing:
data$emits new value.dirty$istrue.- Do NOT mark
<app-dumb>to be checked for changes.
This looked like a prime case for combing Observables:
this.data$ = this.store
.select(fromReducer.getData)
.withLatestFrom(this.dirty$)
.filter(([_, dirty]) => !dirty)
.map(([tree, _]) => tree);
...breaking this down
this.store.select(fromReducer.getData)grab an Observable from ngrx/store using selectors.- Combine newly emitted
data$result with latest result fromdirty$. - Filter on whether
dirty$is false.- If NOT dirty, we have new data from the server and want to update
<app-dumb>. - If dirty, then an edit caused
data$to emit a new value, and we should ignore it.
- If NOT dirty, we have new data from the server and want to update
The problem is <app-smart> is contained within a Material Tab, and as such may be removed (ngOnDestroy) and recreated (ngOnInit) as the user switches tabs. See Life Cycle Hooks.
If and edit has been made, dirty$ is true. If the user then switches tabs and subsequently returns, data$ is filtered out because dirty$ is true:
filter(([_, dirty]) => !dirty)
I have successfully passed the output of Async Pipe into the child <app-dumb> component. But. When the condition fails, I want to pass in the previously emitted value from data$ - and this is where I could really do with some help.