Is it possible to use data from state A inside a selector in state B? - ngxs

I am trying out Ngxs as a state management system and came across a specific use case that I can't seem to figure out. In this use case I am using two normalized objects (I removed some unnecessary fields for readability).
export interface Section {
id: number;
sequence: number;
name: string;
subName: string;
totalQuestions: number;
completedQuestions: number;
selected: boolean;
questionFlows: QuestionFlow[];
}
export interface QuestionFlow {
id: number;
contractId: number;
parentId: number;
subSectionId: number;
path: string;
question: string;
type: string;
answer: string;
completed: boolean;
sequenceNumber: number;
selected: boolean;
questionFlows: QuestionFlow[];
}
These two objects reside in separate stores. A SectionStore and a QuestionFlowStore. The state models are like follows:
export class SectionsStateModel {
sections: { [id: number]: Section };
currentSection: Section;
}
export class QuestionFlowsStateModel {
questionFlows: { [id: number]: QuestionFlow };
currentQuestionFlow: QuestionFlow;
}
Now I would like to create a selector in the QuestionFlowsState that returns every questionFlow that belongs to the currentSection. Is it possible to get the currentSection inside a selector that resides inside the QuestionFlowState while the currentSection resides inside the SectionState? I have tried the code below (with a filled store) without success.
import { SectionsStateModel } from './sections.state';
#State<QuestionFlowsStateModel>({
name: 'questionFlows',
defaults: {
questionFlows: {},
currentQuestionFlow: null
}
})
export class QuestionFlowsState {
#Selector()
static getQuestionFlowsArrayFromCurrentSection(
state: QuestionFlowsStateModel,
sectionState: SectionsStateModel
) {
const questionFlowsFromCurrentSection: QuestionFlow[] = [];
sectionState.currentSection.questionFlows.forEach(questionFlow => {
questionFlowsFromCurrentSection.push(state.questionFlows[+questionFlow]);
});
return questionFlowsFromCurrentSection;
}
}
If there is anything missing/unclear in the question, please let me know.
EDIT:
After some back and forth with #Danny Blue we've come to the solution of adding a parent state that takes the states that contain the data needed for the selector as children (which can be set in the #State decorator). To access the data from these children stores you'll need to call the state.. and you are good to go. Below is the final code that solves my problem.
import { State, Selector } from '#ngxs/store';
import { SectionsState } from './sections.state';
import { QuestionFlowsState } from './question-flows.state';
import { QuestionFlow } from '../../contract-details.model';
import { SectionsStateModel } from './sections.state';
import { QuestionFlowsStateModel } from './question-flows.state';
#State({
name: 'parent',
children: [SectionsState, QuestionFlowsState]
})
export class ParentState {
#Selector()
static getParentFlowsArrayFromCurrentSection(
state
) {
const questionFlowsFromCurrentSection: QuestionFlow[] = [];
state.sections.currentSection.questionFlows.forEach(questionFlow => {
questionFlowsFromCurrentSection.push(
state.questionFlows.questionFlows[+questionFlow]
);
});
return questionFlowsFromCurrentSection;
}
}

You could add the selector at a parent state class which would give it access to both child state.
https://ngxs.gitbook.io/ngxs/advanced/sub-states

Related

Prisma / Graphql resolver

Good morning all,
I m currently back in the famous world of web development and I have in mind to develop a tool by using Nest/Prisma/Graphl.
However, I'm struggling a little bit on key element like the following one.
Basically, I can see that, by using the "include" function in Prisma (module.service.ts), I'm getting subModules list: this is the expected behavior.
However, on Graphl side, to cover field resolver (module.resolver.ts), I can see that the same request is executing again to cover SubModules field.....
What am I missing?????
See below the code:
module.module.ts
import { Field, ID, ObjectType } from '#nestjs/graphql'
import { SubModule } from './submodule.model'
#ObjectType()
export class Module {
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Field((type) => ID)
id: number
name: string
description: string
icon: string
active: boolean
position: number
subModules?: SubModule[]
}
submodule.model.ts
import { Field, ObjectType, ID } from '#nestjs/graphql';
import { Module } from './module.model';
#ObjectType()
export class SubModule {
#Field((type) => ID)
id: number;
name: string;
description: string;
icon: string;
active: boolean;
position: number;
module: Module;
}
module.resolver.ts
import {
Resolver,
Query,
ResolveField,
Parent,
Args,
InputType,
} from '#nestjs/graphql'
import { Module } from 'src/models/module.model'
import { ModuleService } from './module.service'
import { SubModuleService } from './sub-module.service'
#InputType()
class FilterModules {
name?: string
description?: string
icon?: string
active?: boolean
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Resolver((of) => Module)
export class ModuleResolver {
constructor(
private moduleService: ModuleService,
private subModuleService: SubModuleService,
) {}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Query((returns) => Module)
async module(#Args('ModuleId') id: number) {
return this.moduleService.module(id)
}
// eslint-disable-next-line #typescript-eslint/no-unused-vars
#Query((returns) => [Module], { nullable: true })
async modules(
#Args({ name: 'skip', defaultValue: 0, nullable: true }) skip: number,
#Args({ name: 'filterModules', defaultValue: '', nullable: true })
filterModules: FilterModules,
) {
return this.moduleService.modules({
skip,
where: {
name: {
contains: filterModules.name,
},
},
})
}
#ResolveField()
async subModules(#Parent() module: Module) {
const { id } = module
return this.subModuleService.subModules({ where: { moduleId: id } })
}
}
module.service.ts
import { Injectable } from '#nestjs/common'
import { PrismaService } from 'src/prisma.service'
import { Prisma, Module } from '#prisma/client'
#Injectable()
export class ModuleService {
constructor(private prisma: PrismaService) {
prisma.$on<any>('query', (event: Prisma.QueryEvent) => {
console.log('Query: ' + event.query)
console.log('Params' + event.params)
console.log('Duration: ' + event.duration + 'ms')
})
}
async module(id: number): Promise<Module | null> {
return this.prisma.module.findUnique({
where: {
id: id || undefined,
},
})
}
async modules(params: {
skip?: number
take?: number
cursor?: Prisma.ModuleWhereUniqueInput
where?: Prisma.ModuleWhereInput
orderBy?: Prisma.ModuleOrderByWithRelationInput
}): Promise<Module[]> {
const { skip, take, cursor, where, orderBy } = params
return this.prisma.module.findMany({
skip,
take,
cursor,
where,
orderBy,
})
}
async updateModule(params: {
where: Prisma.ModuleWhereUniqueInput
data: Prisma.ModuleUpdateInput
}): Promise<Module> {
const { where, data } = params
return this.prisma.module.update({
data,
where,
})
}
}
Thanks in advance for your help

Generic TypeScript type referencing nested interface types dynamically

Sorry for the confusing title! I'll try to be as clear as possible here. Given the following interface (generated by openapi-typescript as an API definition):
TypeScript playground to see this in action
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
The above interface paths will have many paths identified by a string, each having some methods available which then define the parameters and response type.
I am trying to write a generic apiCall function, such that given a path and a method knows the types of the parameters required, and the return type.
This is what I have so far:
type Path = keyof paths;
type PathMethods<P extends Path> = keyof paths[P];
type RequestParams<P extends Path, M extends PathMethods<P>> =
paths[P][M]['parameters'];
type ResponseType<P extends Path, M extends PathMethods<P>> =
paths[P][M]['responses'][200]['schema'];
export const apiCall = (
path: Path,
method: PathMethods<typeof path>,
params: RequestParams<typeof path, typeof method>
): Promise<ResponseType<typeof path, typeof method>> => {
const url = path;
console.log('params', params);
// method & url are
return fetch(url, { method }) as any;
};
However this won't work properly and I get the following errors:
paths[P][M]['parameters']['path'] -> Type '"parameters"' cannot be used to index type 'paths[P][M]'
Even though it does work (If I do type test = RequestParams<'/v1/some-path/:id', 'get'> then test shows the correct type)
Any idea how to achieve this?
Solution
After few trials, this is the solution I found.
First, I used a conditional type to define RequestParams:
type RequestParams<P extends Path, M extends PathMethods<P>> =
"parameters" extends keyof paths[P][M]
? paths[P][M]["parameters"]
: undefined;
Because typescript deduces the type of path on the fly, the key parameters may not exist so we cant use it. The conditional type checks this specific case.
The same can be done for ResponseType (which will be more verbose) to access the properties typescript is complaining about.
Then, I updated the signature of the function apiCall:
export const apiCall = <P extends Path, M extends PathMethods<P>>(
path: P,
method: M,
params: RequestParams<P, M>
): Promise<ResponseType<P, M>> => {
//...
};
So now the types P and M are tied together.
Bonus
Finally, in case there is no parameter needed, I made the parameter params optional using a conditional type once again:
export const apiCall = <P extends Path, M extends PathMethods<P>>(
path: P,
method: M,
...params: RequestParams<P, M> extends undefined ? []: [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
//...
};
Here is a working typescript playground with the solution. I added a method delete with no params just to test the final use case.
Sources
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
https://stackoverflow.com/a/52318137/6325822
Edit
Here the updated typescript playground with on errors.
Also, I saw that Alessio's solution only works for one path which is a bit limitating. The one I suggest has no errors and works for any number of paths.
I checked Baboo's solution by following the link to his TypeScript playground.
At line 57, the ResponseType type gives the following error:
Type '"responses"' cannot be used to index type 'paths[P][M]'.(2536)
Type '200' cannot be used to index type 'paths[P][M]["responses"]'.(2536)
Type '"schema"' cannot be used to index type 'paths[P][M]["responses"][200]'.(2536)
I did some work starting from that solution, and obtained the functionality required without errors, and using slightly simpler type definitions, which require less type params.
In particular, my PathMethod type does not need any type param, and my RequestParams and ResponseType types need only 1 type param.
Here is a TypeScript playground with the full solution.
As requested in the comments by captain-yossarian, here is the full solution:
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
delete: {
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
type Path = keyof paths;
type PathMethod = keyof paths[Path];
type RequestParams<T extends PathMethod> = paths[Path][T] extends {parameters: any} ? paths[Path][T]['parameters'] : undefined;
type ResponseType<T extends PathMethod> = paths[Path][T] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[Path][T]['responses'][200]['schema'] : undefined;
export const apiCall = <P extends Path, M extends PathMethod>(
path: P,
method: M,
...params: RequestParams<M> extends undefined ? [] : [RequestParams<M>]
): Promise<ResponseType<M>> => {
const url = path;
console.log('params', params);
return fetch(url, { method }) as any;
};
UPDATE:
In the comments, aurbano noted that my solution only worked if paths has only 1 key. Here is an updated solution that works with 2 different paths.
export interface paths {
'/v1/some-path/:id': {
get: {
parameters: {
query: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
post: {
parameters: {
body: {
name: string;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
delete: {
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
'/v2/some-path/:id': {
patch: {
parameters: {
path: {
id: number;
};
header: {};
};
responses: {
/** OK */
200: {
schema: {
id: number;
name: string;
};
};
};
};
};
}
type Path = keyof paths;
type PathMethod<T extends Path> = keyof paths[T];
type RequestParams<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {parameters: any} ? paths[P][M]['parameters'] : undefined;
type ResponseType<P extends Path, M extends PathMethod<P>> = paths[P][M] extends {responses: {200: {schema: {[x: string]: any}}}} ? keyof paths[P][M]['responses'][200]['schema'] : undefined;
export const apiCall = <P extends Path, M extends PathMethod<P>>(
path: P,
method: M,
...params: RequestParams<P, M> extends undefined ? [] : [RequestParams<P, M>]
): Promise<ResponseType<P, M>> => {
const url = path;
console.log('params', params);
return fetch(url, { method: method as string }) as any;
};
apiCall("/v1/some-path/:id", "get", {
header: {},
query: {
id: 1
}
}); // Passes -> OK
apiCall("/v2/some-path/:id", "get", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v2/some-path/:id", "patch", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v2/some-path/:id", "patch", {
header: {},
path: {
id: 1,
}
}); // Passes -> OK
apiCall("/v1/some-path/:id", "get", {
header: {},
query: {
id: 'ee'
}
}); // Type error -> OK
apiCall("/v1/some-path/:id", "get", {
query: {
id: 1
}
}); // Type error -> OK
apiCall("/v1/some-path/:id", "get"); // Type error -> OK
apiCall("/v1/some-path/:id", 'delete'); // Passes -> OK
apiCall("/v1/some-path/:id", "delete", {
header: {},
query: {
id: 1
}
}); // Type error -> OK
And here is an updated playground.

Reuse selector in other selectors in NGXS

I have two classes PizzasState and ToppingsState.
PizzaState already has selector to get selected pizza.
#State<PizzaStateModel>({
name: 'pizzas',
defaults: initialState
})
export class PizzasState {
constructor(private pizzaService: PizzasService) {
}
#Selector([RouterState])
static getSelectedPizza(
state: PizzaStateModel,
routerState: RouterStateModel<RouterStateParams>
): Pizza {
const pizzaId = routerState.state && routerState.state.params.pizzaId;
return pizzaId && state.entities[pizzaId];
}
#Selector()
getPizzaVisualized(state: PizzaStateModel): Pizza {
//
// what is here?
//
}
}
and ToppingsState has selectedToppings
#State({
name: 'toppings',
defaults: initialState
})
export class ToppingsState {
constructor(private toppingsService: ToppingsService) {
}
#Selector()
static selectedToppings(state: ToppingsStateModel): number[] {
return state.selectedToppings;
}
Now I want to join my selected pizza with selected toppings and get my visualized pizza.
How can I reuse getSelectedPizza and getSelectedToppings correctly?
Thank you
It looks like you need to use a joining selector.
#Selector([ToppingsState.selectedToppings])
getPizzaVisualized(state: PizzaStateModel, selectedToppings: number[]): Pizza {
// User data from both states.
}

Only reloading resolves without reloading html

How can i force ui router to reload the resolves on my state without reloading the entire ui/controller since
I am using components and since the data is binded from the state resolve,
i would like to change some parameters (pagination for example) without forcing the entire ui to reload but just the resolves
resolve : {
data: ['MailingListService', '$transition$', function (MailingListService, $transition$) {
var params = $transition$.params();
var ml = params.id;
return MailingListService.getUsers(ml, params.start, params.count)
.then(function (result) {
return {
users: result.data,
totalCount: result.totalCount
}
})
}],
node: ['lists', '$transition$', function (lists, $transition$) {
return _.find(lists, {id: Number($transition$.params().id)})
}]
},
I would like to change $transition$.params.{start|count} and have the resolve updated without reloading the html.
What you requested is not possible out of the box. Resolves are only resolved, when the state is entered.
But: one way of refreshing data could be, to check for state parameter changes in $doCheck and bind them to the components by hand.
Solution 1
This could look something like this:
export class MyComponent {
constructor($stateParams, MailingListService) {
this.$stateParams = $stateParams;
this.MailingListService = MailingListService;
this.paramStart = null;
this.paramCount = null;
this.paramId = null;
this.data = {};
}
$doCheck() {
if(this.paramStart !== this.$stateParams.start ||
this.paramCount !== this.$stateParams.count ||
this.paramId !== this.$stateParams.id) {
this.paramStart = this.$stateParams.start;
this.paramCount = this.$stateParams.count;
this.paramId = this.$stateParams.id;
this.MailingListService.getUsers(this.paramId, this.paramStart, this.paramCount)
.then((result) => {
this.data = {
users: result.data,
totalCount: result.totalCount
}
})
}
}
}
Then you have no binding in the parent component anymore, because it "resolves" the data by itself, and you have to bind them to the child components by hand IF you insert them in the template of the parent component like:
<my-component>
<my-child data="$ctrl.data"></my-child>
</my-component>
If you load the children via views, you are obviously not be able to bind the data this way. There is a little trick, but it's kinda hacky.
Solution 2
At first, resolve an empty object:
resolve : {
data: () => {
return {
value: undefined
};
}
}
Now, assign a binding to all your components like:
bindings: {
data: '<'
}
Following the code example from above, where you resolve the data in $doCheck, the data assignment would look like this:
export class MyComponent {
[..]
$doCheck() {
if(this.paramStart !== this.$stateParams.start ||
this.paramCount !== this.$stateParams.count ||
this.paramId !== this.$stateParams.id) {
[..]
this.MailingListService.getUsers(this.paramId, this.paramStart, this.paramCount)
.then((result) => {
this.data.value = {
users: result.data,
totalCount: result.totalCount
}
})
}
}
}
And last, you check for changes in the child components like:
export class MyChild {
constructor() {
this.dataValue = undefined;
}
$doCheck() {
if(this.dataValue !== this.data.value) {
this.dataValue = this.data.value;
}
}
}
In your child template, you access the data with:
{{ $ctrl.dataValue | json }}
I hope, I made my self clear with this hack. Remember: this is a bit off the concept of UI-Router, but works.
NOTE: Remember to declare the parameters as dynamic, so changes do not trigger the state to reload:
params: {
start: {
dynamic: true
},
page: {
dynamic: true
},
id: {
dynamic: true
}
}

Angular2: how to know if the browser reloads or not?

This is a paragraph from ng-book2: Javascript, by default, propagates the click event to all the parent components. Because the click event is propagated to parents, our browser is trying to follow the empty link.
To fix that, we need to make the click event handler to return false. This will ensure the browser won’t try to refresh the page.
Question: how to know if the browser reloads or not? I can not detect it by eyes.
Here is part of my code:
import { Component } from "angular2/core";
#Component({
selector: 'reddit-article',
inputs: ['article'],
templateUrl: 'app/redditArticle.html'
})
export class RedditArticleComponent {
article: Article;
constructor() {
console.log('Just loaded!');
}
voteUp() {
this.article.voteUp();
}
voteDown() {
this.article.voteDown();
}
}
export class Article {
//title: string;
//link: string;
//votes: number;
constructor(public title: string, public link: string, public votes: number) {
//this.title = title;
//this.link = link;
//this.votes = votes;
}
voteUp(): boolean {
this.votes++;
return false;
}
voteDown(): boolean {
this.votes--;
return false;
}
}
export class AppComponent {
constructor() {
console.log('just loaded');
}
}
When you open the browser dev tools you know a reload happened when an additional just loaded is printed. Ensure [x] preserve log is checked in the console (Chrome).

Resources