skip to Main Content

I know that Angular Signals could be used in this particular case in order to avoid calling the isVisible function constantly. But I have no experience with this new feature and I cannot figure out what to change in my code to take advantage of Signals. I pasted a simplified snippet of my code below, just for the general idea.

What should I change to make this work with Signals ?

visibleSections = [];
sectionConfig = [
{
  name: 'someComponent',
  hidden: false
},
{
  name: 'otherComponent',
  hidden: false
}
];


ngOnInit() {
   this.visibleSections = this.sectionConfig.filter(c => !c.hidden);
}
 
public isVisible(section): boolean {
  return this.visibleSections.findIndex(s => s.name === section) > -1;
}

public setSectionVisibility(e: any): void {
this.sectionConfig.find(c => c.name === e.itemValue.name).hidden = e.value.findIndex(p =>  p === e.itemValue) === -1;
}

HTML :

<p-multiSelect
  #multiselect
  defaultLabel="Show/hide sections"
  optionLabel="title"
  name="show-hide-sections"
  scrollHeight="300px"
  styleClass="multiselect-as-button mr-3 ml-3"
  dropdownIcon="fas fa-bars rotate-90"
  [options]="sectionConfig"
  [(ngModel)]="visibleSections"
  [displaySelectedLabel]="false"
  (onChange)="setSectionVisibility($event)"
  [style]="{ width: '208px' }"
  [showHeader]="false"
  [appendTo]="'body'"></p-multiSelect>


@if (isVisible("someComponent")) {
  <some-component></some-component>
}

@if (isVisible("otherComponent")) {
  <other-component></other-component>
}
      

2

Answers


  1. The trick I use is to start with the state in a Signal/WritableSignal. This is usually at a component/service boundary, so it’s a good candidate for input() or model() too:

    type Config = { name: string; hidden: boolean; };
    
    const sectionConfig = signal<Config[]>([]);
    

    Then I compose from there using computed(). Whether it’s whole slices:

    const visibleSections = computed(() => {
      const sections = this.sectionConfig();
      return sections.filter(s => !s.isHidden);
    });
    

    Or individual keys:

    const isSomeComponentVisible = computed(() => {
      const sections = this.visibleSections();
      return sections.find(s => s.name === 'someComponent') > -1;
    });
    
    const isOtherComponentVisible = computed(() => {
      const sections = this.visibleSections();
      return sections.find(s => s.name === 'otherComponent') > -1;
    });
    

    Then I bind to the slices where they’re needed:

    @if (isSomeComponentVisible()) {
      <some-component></some-component>
    }
    
    @if (isOtherComponentVisible()) {
      <other-component></other-component>
    }
    

    The trick is to realize that the only state you have to track is sectionConfig. All the other state (including visibleSections) is derived state. It would be tempting to create another Signal/WritableSignal for visibleSections and then use effect() to update it when sectionConfig changes, but that isn’t necessary and would only lead to more problems (confusion, having to set allowSignalWrites, etc).


    This is super similar to the way I use RxJS too. It’s composing a pipeline from the original data (the list of configurations) to the desired data (the visibility of certain keys).

    The downside is that the individual properties aren’t dynamic. You’ll need to create an is_X_Visible() signal for each. If you don’t want to create these (or you cannot because you don’t know the keys until runtime), you’ll need to introduce a Pipe (called the same way you would any other parameter-based pipe: @if (visibleSections() | contains:'someComponent')) or you will need to adjust the shape of the final composition to accommodate the desired runtime lookup:

    const keyToVisibleMap = computed(() => {
      const sections = this.visibleSections();
      return sections.reduce((keys, section) => {
        keys[section.name] = !section.hidden;
        return keys;
      }, {});
    });
    
    @if (keyToVisibleMap()['someComponent']) {
      <some-component></some-component>
    }
    
    @if (keyToVisibleMap()['otherComponent']) {
      <other-component></other-component>
    }
    

    Then for updating, you just pass a new value to the top-level Signal/WritableSignal and the rest will cascade down the reactive tree for you:

    type SetSectionVisibilityEvent = {
      itemValue: { name: string };
      value: { name: string }[];
    }
    
    public setSectionVisibility(e: SetSectionVisibilityEvent): void {
      const newConfig = [...this.sectionConfig()];
      const section = newConfig.find(s => s.name === e.itemValue.name);
      if (section) {
        section.hidden = e.value.findIndex(p => p === e.itemValue) === -1;
      }
      this.sectionConfig.set(newConfig);
    }
    
    Login or Signup to reply.
  2. The first step is to convert sectionConfig to a primary signal.

    sectionConfig = signal([
      {
        name: 'someComponent',
        hidden: false,
      },
      {
        name: 'otherComponent',
        hidden: false,
      },
    ]);
    

    Then, the property visibleSections is a derived state, so we use linkedSignal to derive the state from the original sectionConfig (Notice I am using linkedSignal instead of computed because you have two way binded to the HTML [(ngModel)]="visibleSections", as you know computed is not writable so we use linkedSignal).

    visibleSections = linkedSignal(() => {
      return this.sectionConfig().filter((item: any) => !item.hidden);
    });
    

    When we want to filter using UI, we only need the string value, so we create another derived state to determine the string of components that need to be enabled.

    visibleSectionsFlags = computed(() => {
      return this.sectionConfig()
        .filter((item: any) => !item.hidden)
        .map((item: any) => item.name);
    });
    

    The change method, we leverage update method, which provides the value of the signal. Here we lookup the selected value and then toggle the flag. The most important point you need to notice is that, we use array destructuring to create a new memory reference (Arrays and objects are stored as memory references), the signal checks the value has been changed. So we must change the memory reference of the array only then signal will consider as the value to be changed and the derived states will be recomputed.

    public setSectionVisibility(e: any): void {
      this.sectionConfig.update((sectionConfig: any) => {
        const foundIndex = this.sectionConfig().findIndex(
          (c) => c.name === e.itemValue.name
        );
        if (foundIndex > -1) {
          sectionConfig[foundIndex].hidden = !sectionConfig[foundIndex].hidden;
        }
        return [...sectionConfig];
      });
    }
    

    In the HTML side, we can bind the visibleSections to ngModel and then to hide/show the components, we use the array method includes to check if the component should be shown or not visibleSectionsFlags().includes("someComponent").

    Full Code:

    TS:

    import {
      Component,
      OnInit,
      computed,
      linkedSignal,
      signal,
    } from '@angular/core';
    import { ImportsModule } from './imports';
    import { JsonPipe } from '@angular/common';
    interface City {
      name: string;
      code: string;
    }
    
    @Component({
      selector: 'app-a',
      template: `a comp`,
      standalone: true,
    })
    export class A {}
    
    @Component({
      selector: 'app-b',
      template: `b comp`,
      standalone: true,
    })
    export class B {}
    
    @Component({
      selector: 'multi-select-basic-demo',
      templateUrl: './multi-select-basic-demo.html',
      standalone: true,
      imports: [ImportsModule, A, B, JsonPipe],
    })
    export class MultiSelectBasicDemo {
      sectionConfig = signal([
        {
          name: 'someComponent',
          hidden: false,
        },
        {
          name: 'otherComponent',
          hidden: false,
        },
      ]);
    
      visibleSections = linkedSignal(() => {
        return this.sectionConfig().filter((item: any) => !item.hidden);
      });
    
      visibleSectionsFlags = computed(() => {
        return this.sectionConfig()
          .filter((item: any) => !item.hidden)
          .map((item: any) => item.name);
      });
    
      public setSectionVisibility(e: any): void {
        this.sectionConfig.update((sectionConfig: any) => {
          const foundIndex = this.sectionConfig().findIndex(
            (c) => c.name === e.itemValue.name
          );
          if (foundIndex > -1) {
            sectionConfig[foundIndex].hidden = !sectionConfig[foundIndex].hidden;
          }
          return [...sectionConfig];
        });
      }
    }
    

    HTML:

    <p-multiSelect
      #multiselect
      defaultLabel="Show/hide sections"
      optionLabel="name"
      name="show-hide-sections"
      scrollHeight="300px"
      styleClass="multiselect-as-button mr-3 ml-3"
      dropdownIcon="fas fa-bars rotate-90"
      [options]="sectionConfig()"
      [(ngModel)]="visibleSections"
      [displaySelectedLabel]="false"
      (onChange)="setSectionVisibility($event)"
      [style]="{ width: '208px' }"
      [showHeader]="false"
      [appendTo]="'body'"
    ></p-multiSelect>
    <br /><br />
    @if (visibleSectionsFlags().includes("someComponent")) {
    <app-a></app-a><br /><br />
    } @if (visibleSectionsFlags().includes("otherComponent")) {
    <app-b></app-b><br /><br />
    }
    

    Stackblitz Demo

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search