Change detection for nested objects in a state - ngxs

Let say we have a state with the following structure :
#State<StateModel>({
name: 'pg',
defaults: {
docs: {
doc1: {
id: 'doc1',
content: 'foo',
}
doc2: {
id: 'doc2',
content: 'bar'
}
}
}
How is change detection working ? If I do the change
#Action(UpdateContent)
update(ctx: StateContext<StateModel>) {
ctx.patchState( { docs.doc2.content: 'something else' } );
}
None of the following observable emit a value :
obs1$ = store.select( state => state.docs );
obs2$ = store.select( state => state.docs.docs2 );
obs2$ = store.select( state => state.docs.docs2.content );
I am not an expert in rxjs programming but it seems strange, any ideas ?

Try adding state name pg like this obs1$ = store.select( state => state.pg.docs ) instead.
The state name is not required if you are selecting via #Selector().
I assume your patchState() code's object notation is just an example.

Related

Modify Observable array before subscribing by rxjs operators

Having two models look like this:
export class Game {
id: number;
name: string;
platform: number;
}
export class Platform {
id: number;
name: string;
}
having an observable array of the Game object that has a property of platformId which is related to the Platform object. for better understand, I created two separate methods for getting a list of my games and another method for getting a platform based on id.
getGames(): Observable<Game[]> {
return of([
{
id: 1,
name: 'god of war',
platform: 1,
},
{
id: 2,
name: 'halo',
platform: 2,
},
]);
}
getPlatform(id): Observable<Platform> {
if (id === 1)
return of({
id: 1,
name: 'ps4',
});
if (id === 2)
return of({
id: 2,
name: 'xbox',
});
}
now I'm with help of two operators of rxjs (switchMap,forkJoin) reach to this point:
this.getGames()
.pipe(
switchMap((games: Game[]) =>
forkJoin(
games.map((game) =>
forkJoin([this.getPlatform(game.platform)]).pipe(
map(([platform]) => ({
game: game.name,
platform: platform.name,
}))
)
)
)
)
)
.subscribe((v) => {
console.log(v);
this.gamesV2 = v;
});
and my final result:
[
{
game: "god of war"
platform: "ps4"
},
{
game: "halo"
platform: "xbox"
}
]
is it possible to achieve this in a simpler way?
StackBlitz
By flattening an inner observable array of getPlatformV2:
this.getGames()
.pipe(
switchMap(games =>
combineLatest(
games.map(game =>
this.getPlatformV2(game.id).pipe(
map(platform => ({
game: game.name,
platform
}))
)
)
)
)
)
Extra: Regarding Game and Platform, you should use TS types or interfaces instead of classes if you won't implement a constructor on those.
I find another way in StackOverflow thanks to Daniel Gimenez and putting it here, if anyone has better and simpler, I really appreciate it to share it with me.
Create another method which returns the name of the platform:
getPlatformV2(id): Observable<string> {
const platforms = [
{
id: 1,
name: 'ps4',
},
{
id: 2,
name: 'xbox',
},
];
return of(platforms.find(x=>x.id===id).name);
}
}
and instead of using two forkjoin, I used concatMap
this.getGames()
.pipe(
switchMap((games) => games),
concatMap((game) =>
forkJoin({
game: of(game.name),
platform: this.getPlatformV2(game.platform),
})
),
toArray()
)
.subscribe(console.log);

Not able to find the string modified in subsequent filters using tap operator

I have implemented a logic in my angular component that will display the version number against the Agreement.pdf like Agreement_v1.pdf as well as set the editable flag to true to any document that is not Identification and also Agreement document that is not v1. As you can see the version number logic is applied in the first tap operator. Unfortunately I think my second tap operator does find such occurrence. Could somebody tell me why ?
var first: number = 1;
if (this.readOnly) {
this.columnsToDisplay = ['category', 'filename', 'uploadedOnDate'];
} else {
this.columnsToDisplay = ['category', 'filename', 'uploadedOnDate', 'action', 'delete'];
}
const investigationDocuments = this.caseChange$
.pipe(
map(investigationsCase => {
this.configureSendNewAgreement(investigationsCase.state.documents);
return investigationsCase.state.documents
? investigationsCase.state.documents.filter(doc => doc.isActive === true)
: [];
}),
tap(a => a.filter(d => d.type === TypeOfDocument.Agreement).sort((x, y) => +new Date(x.uploadedOnDate) - +new Date(y.uploadedOnDate)).forEach(b => b.name = 'Agreement_v' + (first++) +'.pdf')),
tap(a => a.filter(d => d.type !== TypeOfDocument.Identification && d.uploadedBy != null && d.name !== 'Agreement_v1.pdf').forEach(b => b.isEditable = true)),
);
The tap operator doesn't modify the incoming value. So anything you'd do to a within the tap is ignored.
From the docs:
Used when you want to affect outside state with a notification without altering the notification
In other words, only use tap if you want to do some debugging or a side effect that does not change the notification (incoming variable).
Try changing both tap operators to map and see if that helps.
The map allows changing the incoming variable and it emits that changed variable to the next operator in the pipeline.
I did a stackblitz to show the difference:
https://stackblitz.com/edit/angular-tap-vs-map-deborahk
Component
import { Component, VERSION } from '#angular/core';
import { of } from 'rxjs';
import { map, tap } from 'rxjs/operators';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular ' + VERSION.major;
result$ = of(SampleData).pipe(
tap(a => a.filter(d => !d.startsWith('t'))),
tap(a => a.filter(d => !d.startsWith('f')))
)
result2$ = of(SampleData).pipe(
map(a => a.filter(d => !d.startsWith('t'))),
map(a => a.filter(d => !d.startsWith('f')))
)
constructor() {
this.result$.subscribe(result => console.log('Result: ', JSON.stringify(result)))
this.result2$.subscribe(result => console.log('Result2: ', JSON.stringify(result)))
}
}
export const SampleData = [
"one",
"two",
"three",
"four",
"five"
]
Result
Result: ["one","two","three","four","five"]
Result2: ["one"]
Notice that the first statement uses tap and the result is unchanged from the original sample data array.
The second statement uses map and the filtered results are propagated through the pipeline and appear in the result.

RxJS logic which solves a filter/merge issue

This is more a logical problem then a RxJS problem, I guess, but I do not get it how to solve it.
[input 1]
From a cities stream, I will receive 1 or 2 objects (cities1 or cities2 are test fixtures).
1 object if their is only one language available, 2 objects for a city with both languages.
[input 2]
I do also have a selectedLanguage ("fr" or "nl")
[algo]
If the language of the object corresponds the selectedLanguage, I will pluck the city. This works for my RxJS when I receive 2 objects (cities2)
But since I also can receive 1 object, the filter is not the right thing to do
[question]
Should I check the cities stream FIRST if only one object exists and add another object. Or what are better RxJS/logical options?
const cities1 = [
{city: "LEUVEN", language: "nl"}
];
const cities2 = [
{city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
const selectedLang = "fr"
const source$ = from(cities1);
const result = source$.pipe(
mergeMap((city) => {
return of(selectedLang).pipe(
map(lang => {
return {
lang: city.language,
city: city.city,
selectedLang: lang
}
}),
filter(a => a.lang === selectedLang),
pluck('city')
)
}
)
);
result.subscribe(console.log)
If selectedLang is not an observable (i.e. you don't want this to change) then I think it would make it way easier if you keep it as a value:
const result = source$.pipe(
filter(city => city.language === selectedLang)
map(city => city.city)
);
There's nothing wrong from using external parameters, and it makes the stream easier to read.
Now, if selectedLang is an observable, and you want result to always give the city with that selectedLang, then you probably need to combine both streams, while keeping all the cities received so far:
const selectedLang$ = of(selectedLang); // This is actually a stream that can change value
const cities$ = source$.pipe(
scan((acc, city) => [...acc, city], [])
);
const result = combineLatest([selectedLang$, cities$]).pipe(
map(([selectedLang, cities]) => cities.find(city => city.language == selectedLang)),
filter(found => Boolean(found))
map(city => city.city)
)
Edit: note that this result will emit every time cities$ or selectedLang$ changes and one of the cities matches. If you don't want repeats, you can use the distinctUntilChanged() operator - Probably this could be optimised using an exhaustMap or something, but it makes it harder to read IMO.
Thanks for your repsonse. It's great value for me. Indeed I will forget about the selectedLang$ and pass it like a regular string. Problem 1 solved
I'll explain a bit more in detail my question. My observable$ cities$ in fact is a GET and will always return 1 or 2 two rows.
leuven:
[ { city: 'LEUVEN', language: 'nl', selectedLanguage: 'fr' } ]
brussel:
[
{ city: 'BRUSSEL', language: 'nl', selectedLanguage: 'fr' },
{ city: 'BRUXELLES', language: 'fr', selectedLanguage: 'fr' }
]
In case it returns two rows I will be able to filter out the right value
filter(city => city.language === selectedLang) => BRUXELLES when selectedLangue is "fr"
But in case I only receive one row, I should always return this city.
What is the best solution to this without using if statements? I've been trying to work with object destruct and scaning the array but the result is always one record.
// HTTP get
const leuven: City[] = [ {city: "LEUVEN", language: "nl"} ];
// same HTTP get
const brussel: City[] = [ {city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
mapp(of(brussel), "fr").subscribe(console.log);
function mapp(cities$: Observable<City[]>, selectedLanguage: string): Observable<any> {
return cities$.pipe(
map(cities => {
return cities.map(city => { return {...city, "selectedLanguage": selectedLanguage }}
)
}),
// scan((acc, value) => [...acc, { ...value, selectedLanguage} ])
)
}

How can the evaluation of a ngrx-store selector be controlled?

I have a selector:
const mySelector = createSelector(
selectorA,
selectorB,
(a, b) => ({
field1: a.field1,
field2: b.field2
})
)
I know the selector is evaluated when any of its inputs change.
In my use case, I need to control "mySelector" by a third selector "controlSelector", in the way that:
if "controlSelector" is false, "mySelector" does not evaluate a new value even in the case "selectorA" and/or "selectorB" changes, and returns the memoized value
if "controlSelector" is true, "mySelector" behaves normally.
Any suggestions?
Selectors are pure functions..its will recalculate when the input arguments are changed.
For your case its better to have another state/object to store the previous iteration values.
You can pass that as selector and based on controlSelector value you can decide what you can return.
state : {
previousObj: {
...
}
}
const prevSelector = createSelector(
...,
(state) => state.previousObj
)
const controlSelector = createSelector(...);
const mySelector = createSelector(
controlSelector,
prevSelector,
selectorA,
selectorB,
(control, a, b) => {
if(control) {
return prevSelector.previousObj
} else {
return {
field1: a.field1,
field2: b.field2
};
}
}
)
Sorry for the delay...
I have finally solved the issue not using NGRX selectors to build up those "higher selectors" and creating a class with functions that use combineLatest, filter, map and starWith
getPendingTasks(): Observable<PendingTask[]> {
return combineLatest(
this.localStore$.select(fromUISelectors.getEnabled),
this.localStore$.select(fromUISelectors.getShowSchoolHeadMasterView),
this.memStore$.select(fromPendingTaskSelectors.getAll)).pipe(
filter(([enabled, shmView, tasks]) => enabled),
map(([enabled, shmView, tasks]) => {
console.log('getPendingTasks');
return tasks.filter(task => task.onlyForSchoolHeadMaster === shmView);
}),
startWith([])
);
}
Keeping the NGRX selectors simple and doing the heavy lifting (nothing of that in this example, though) in this kind of "selectors":
- will generate an initial default value (startWith)
- will not generate new value while filter condition fails (that is, when not enabled, any changes in the other observables do not fire a new value of this observable)

Angular2 Model Driven Dynamic Form Validation

Please Refer to my plunkr
I've been playing around with the new Angular 2 RC and I think I have figured out how the form validation works.
First I build 2 objects called defaultValidationMessages and formDefinition
private defaultValidationMessages: { [id: string]: string };
formDefinition: {
[fieldname: string]:
{
displayName: string,
placeholder: string,
currentErrorMessage: string,
customValidationMessages: { [errorKey: string]: string }
defaultValidators: ValidatorFn,
defaultValue: any
}
};
Then I load up those objects with the default validators and field information. and build the ControlGroup from the formDefinition object.
this.defaultValidationMessages = {
'required': '{displayName} is required',
'minlength': '{displayName} must be at least {minlength} characters',
'maxlength': '{displayName} cannot exceed {maxlength} characters',
'pattern': '{displayName} is not valid'
}
this.formDefinition = {
'name': {
displayName: 'Name',
placeholder: '',
currentErrorMessage: '',
customValidationMessages: {},
defaultValidators: Validators.compose(
[
Validators.required,
Validators.minLength(3),
Validators.maxLength(50)
]),
defaultValue: this.person.name
},
'isEmployee': {
displayName: 'Is Employee',
placeholder: '',
currentErrorMessage: '',
customValidationMessages: {},
defaultValidators: Validators.compose([]),
defaultValue: this.person.isEmployee
},
'employeeId': {
displayName: 'Employee Id',
placeholder: '',
currentErrorMessage: '',
customValidationMessages: { 'pattern': '{displayName} must be 5 numerical digits' },
defaultValidators: Validators.compose(
[
Validators.pattern((/\d{5}/).source)
]),
defaultValue: this.person.employeeId
}
}
this.personForm = this.formBuilder.group({});
for (var v in this.formDefinition) {
this.personForm.addControl(v, new Control(this.formDefinition[v].defaultValue, this.formDefinition[v].defaultValidators));
}
this.personForm.valueChanges
.map(value => {
return value;
})
.subscribe(data => this.onValueChanged(data));
Using a technique that I learned from Deborah Kurata's ng-conf 2016 session I bind a method to the ControlGroups valueChanges event.
By defining sets of default validators on each control it allows the control to dynamically append new validators to it based on future action. And then clearing back to the default validators later.
Issue I still have.
I was having an issue getting my typescript intellisense to import the ValidatorFn type. I found it here but I don't think I'm suppose to access it like this:
import { ValidatorFn } from '../../../node_modules/#angular/common/src/forms/directives/validators'
I also had to reset the form by setting some internal members. Is there a better way to reset the form? see below:
(<any> this.programForm.controls[v])._touched = false;
(<any> this.programForm.controls[v])._dirty = false;
(<any> this.programForm.controls[v])._pristine = true;
Please look at my plunk and let me know if there is a better way to handle model driven dynamic form validation?
My import string looks like this and it isn't marked as an error.
import { ValidatorFn } from '#angular/common/src/forms/directives/validators';
And some info about the reset form issue. There is not proper reset feature available yet, but a workaround exists. I've found it in docs.
You need a component field
active: true;
and you need to check it in your form tag:
<form *ngIf="active">
After that you should change your personFormSubmit() method to:
personFormSubmit() {
this.person = new Person();
this.active = false;
setTimeout(()=> {
this.active=true;
this.changeDetectorRef.detectChanges();
alert("Form submitted and reset.");
}, 0);
}
I tried this solution with you plnkr example and seems that it works.

Resources