Adrii
Adrii

Reputation: 1740

Angular material dialog and ngrx

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 !

Upvotes: 8

Views: 5236

Answers (1)

androbin
androbin

Reputation: 1759

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.

Upvotes: 1

Related Questions