I'm working on a new Angular 6 application, with Ngrx and Angular material. I'm creating the base app that will be used by many devs in my company. My problem is on the dialog redux system that I want to create.
I will start by share my actual code and I will explain the problem and what I tried.
My goal : Anywhere in my app, I want to simply call an action that will open a custom dialog (specific to each feature). The app should open multiple fullscreen dialogs.
Here is my simplified architecture :
AppModule
CoreModule
DialogsModule (StoreModule.forFeature('dialog', dialogReducer) / Effects.forFeature([DialogEffects]))
FeatureAModule (contains specific dialogs component)
FeatureBModule (contains specific dialogs component)
That I want, anywhere in my app :
// Random Feature
openDialog(): void {
const payload = {
componentOrTemplateRef: MyDialogComponent, // The dialog, create by dev, in a specific feature
config: {
id: 'my-custom-id',
data: {
... // MAT_DIALOG_DATA
}
}
};
this.store.dispatch(new OpenDialogAction(payload));
}
My actual dialog Redux :
dialog.action.ts
export enum DialogActionTypes {
OPEN = '[DIALOG] OPEN',
SAVE_REF = '[DIALOG] SAVE_REF' // use to store dialog reference in the ngrx store
CLOSE = '[DIALOG] CLOSE'
}
export type DialogAction = OpenDialogAction | SaveRefDialogAction | CloseDialogAction;
export interface OpenDialogPayload {
componentOrTemplateRef: ComponentType<any>;
config: MatDialogConfig;
}
export interface CloseDialogPayload {
dialogId: string;
responseData?: any;
}
export class OpenDialogAction implements Action {
readonly type = DialogActionTypes.OPEN;
constructor(public payload: OpenDialogPayload) {}
}
export class SaveRefDialogAction implements Action {
readonly type = DialogActionTypes.SAVE_REF;
constructor(public payload: MatDialogRef<any>) {}
}
export class CloseDialogAction implements Action {
readonly type = DialogActionTypes.CLOSE;
constructor(public payload: CloseDialogPayload) {}
}
dialog.reducer.ts
export interface DialogState {
refs: Array<{ id: string, ref: MatDialogRef<any> }>;
}
const initialState: DialogState = {
refs: []
};
export function dialogReducer(state: DialogState = initialState, action: DialogAction): DialogState {
switch (action.type) {
case DialogActionTypes.SAVE_REF:
return { ...state, refs: [...state.refs, { id: action.payload.id, ref: action.payload }] };
case DialogActionTypes.CLOSE:
return { ...state, refs: state.refs.filter(ref => ref.id !== action.payload.dialogId) };
default:
return state;
}
}
// DialogState Selector
export const getDialogState = createFeatureSelector('dialog');
// DialogState property selectors
export const getDialogRefById = (id: string) => createSelector(getDialogState, (state: DialogState) => state.refs.find(ref => ref.id === id).ref);
dialog.effects.ts
@Injectable()
export class DialogEffects {
@Effect()
openDialog$: Observable<SaveRefDialogAction> = this.actions$.pipe(
ofType(DialogActionTypes.OPEN),
map((action: OpenDialogAction) => action.payload),
switchMap((payload: OpenDialogPayload) => of(this.dialog.open(payload.componentOrTemplateRef, payload.config))),
map((dialogRef: MatDialogRef<any>) => new SaveRefDialogAction(dialogRef))
);
@Effect({ dispatch: false })
closeDialog$: Observable<{}> = this.actions$.pipe(
ofType(DialogActionTypes.CLOSE),
map((action: CloseDialogAction) => action.payload),
tap((payload: CloseDialogPayload) => this.dialog.getDialogById(payload.dialogId).close(payload.responseData)),
mapTo(of())
);
constructor(private actions$: Actions, private dialog: MatDialog) {}
I had a problem with feature's custom dialog components. They were not recognized by DialogsModule (they must be on entryComponents). So, I created a static method withComponents
that return a ModuleWithProviders and populate entryComponents with the injection token ANALYZE_FOR_ENTRY_COMPONENTS
@NgModule({
imports: [
MatDialogModule,
StoreModule.forFeature('dialog', dialogReducer),
EffectsModule.forFeature([DialogEffects])
]
})
export class DialogsModule {
static withComponents(components: any) ModuleWithProviders {
return {
ngModule: DialogsModule,
providers: [{ provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: components, multi: true }]
};
}
}
The Problem
All of my feature with custom dialog need to import DialogsModule... But, DialogEffects will be instanciated each times (If I have 3 modules that must imports DialogsModule, DialogEffects will be instanciate 3 times).
How can I have a correct material dialog manager without this problem and the entryComponents problem ? I'm open for any suggestions.
Thank you by advance !
When should you not use NgRx? Never use NgRx if your application is a small one with just a couple of domains or if you want to deliver something quickly. It comes with a lot of boilerplate code, so in some scenarios it will make your coding more difficult.
We can pass data to the dialog component by using the data property of the dialog configuration object. As we can see, the whole data object initially passed as part of the dialog configuration object can now be directly injected into the constructor.
Luckily, NgRx provides state and action sanitizers that can be used to clean up your data to reduce the amount of state (for example, multiple arrays with thousands of items) to improve the performance of the devtool.
You can use forRoot and forFeature for the module. Link here
For the root module you add the services to be singleton (like effects). For the feature module you can add the others.
You can use the singleton service with providedIn: 'root'
but I don't know actually if it's working with NgRx effects.
P.S. On the other hand if you restore the state with HMR the modals won't stay open.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With