htmlangularsignalsprimengangular-signals

How to leverage Angular Signals for toggling visibility of my components


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>
}
   

[EDIT]

Following both very good answers below, and considering I'm still using angular 18 and cannot use linkedSignal for now, this is how I ended up doing it :

TS:

type Config = { name: string; hidden: boolean; };

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

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


public 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="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 (visibleSectionsFlags().includes("someComponent")) {
  <app-some-component></app-some-component>
 }

Solution

  • 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