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
pending
totrue
to update the UI. - Results in a HTTP request being made (via ngrx/effects).
- Set
AppActions.GET_NEW_DATA_SUCCESS:
- Set
pending
tofalse
to update the UI. - Set
dirty
tofalse
because no edits have been made.
- Set
AppActions.EDIT_DATA:
- Update data provided in payload.
- Set
dirty
totrue
because 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.