Search code examples
angulartypescriptngrxngrx-store

Dynamic Types in ngrx SignalStore with Entities


I need to store different types of controls (Buttons, Inputs, ...) in a Statemanagement. I would like to use the ngrx SignalStore with Entity Management for that.

Is it possible to define the Type of the Entities dynamic? I have the following type defintions:

export interface Control {
  id: number;
  visible: boolean;
}

export interface ButtonControl extends Control {
  displayText: string;
}

export interface InputControl extends Control {
  placeholder: string;
}

When instantiating the signalStore I can't pass a generic Type:

export const ControlsStore = signalStore(withEntities<Control>());

Using a function with an generic type I wouldn't have a singleton.

Any ideas on this?


Solution

  • If you want all the types to be stored on the same state, you can merge the types using typescript & and make the derived types to be not mandatory using ?:, by doing this you can use multiple types on the same state!

    export interface Control {
      id: number;
      visible: boolean;
    }
    
    export interface ButtonControl extends Control {
      displayText?: string;
    }
    
    export interface InputControl extends Control {
      placeholder?: string;
    }
    export type ControlType = Control & ButtonControl & InputControl;
    
    const TodoStore = signalStore(
      withState({ ids: [] }), // ids property already exists
      withEntities({
        entity: type<ControlType>(),
        collection: 'todo',
      })
    );
    

    FULL CODE:

    import { Component, inject } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { signalStore, withState, type, patchState } from '@ngrx/signals';
    import { addEntities, withEntities } from '@ngrx/signals/entities';
    
    export interface Control {
      id: number;
      visible: boolean;
    }
    
    export interface ButtonControl extends Control {
      displayText?: string;
    }
    
    export interface InputControl extends Control {
      placeholder?: string;
    }
    export type ControlType = Control & ButtonControl & InputControl;
    
    const TodoStore = signalStore(
      withState({ ids: [] }), // ids property already exists
      withEntities({
        entity: type<ControlType>(),
        collection: 'todo',
      })
    );
    
    @Component({
      selector: 'app-root',
      standalone: true,
      template: `
        <ul>
          @for (todo of todoStore.todoEntities(); track todo.id) {
            <li>{{ todo.id }}</li>
          }
        </ul>
      `,
      providers: [TodoStore],
    })
    export class App {
      todoStore = inject(TodoStore);
    
      ngOnInit() {
        patchState(
          this.todoStore,
          addEntities(
            [
              { id: 1, visible: true },
              { id: 2, visible: false, displayText: 'asdf' },
            ],
            { collection: 'todo' }
          )
        );
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo


    If you want to preserve the types, you must have a separate collection for each of the types, with a separate unique collection name!

    export interface Control {
      id: number;
      visible: boolean;
    }
    
    export interface ButtonControl extends Control {
      displayText: string;
    }
    
    export interface InputControl extends Control {
      placeholder: string;
    }
    
    const TodoStore = signalStore(
      withState({ ids: [] }), // ids property already exists
      withEntities({
        entity: type<Control>(),
        collection: 'control',
      }),
      withEntities({
        entity: type<ButtonControl>(),
        collection: 'button',
      }),
      withEntities({
        entity: type<InputControl>(),
        collection: 'input',
      })
    );
    

    FULL CODE:

    import { Component, inject } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { signalStore, withState, type, patchState } from '@ngrx/signals';
    import { addEntities, withEntities } from '@ngrx/signals/entities';
    
    export interface Control {
      id: number;
      visible: boolean;
    }
    
    export interface ButtonControl extends Control {
      displayText: string;
    }
    
    export interface InputControl extends Control {
      placeholder: string;
    }
    
    const TodoStore = signalStore(
      withState({ ids: [] }), // ids property already exists
      withEntities({
        entity: type<Control>(),
        collection: 'control',
      }),
      withEntities({
        entity: type<ButtonControl>(),
        collection: 'button',
      }),
      withEntities({
        entity: type<InputControl>(),
        collection: 'input',
      })
    );
    
    @Component({
      selector: 'app-root',
      standalone: true,
      template: `
        <ul>
          @for (todo of todoStore.controlEntities(); track todo.id) {
            <li>{{ todo.id }}</li>
          }
        </ul>
        <ul>
          @for (todo of todoStore.inputEntities(); track todo.id) {
            <li>{{ todo.placeholder }}</li>
          }
        </ul>
      `,
      providers: [TodoStore],
    })
    export class App {
      todoStore = inject(TodoStore);
    
      ngOnInit() {
        patchState(
          this.todoStore,
          addEntities(
            [
              { id: 1, visible: true },
              { id: 2, visible: false },
            ],
            { collection: 'control' }
          )
        );
        patchState(
          this.todoStore,
          addEntities(
            [
              { id: 1, visible: true, placeholder: 'asdf' },
              { id: 2, visible: false, placeholder: 'asdf2' },
            ],
            { collection: 'input' }
          )
        );
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo