@HostListener(‘click’) and Signal update error

  Kiến thức lập trình

I’m trying to update a service containing signal inside an @HostListener binding in my component.

It work perfectly in the DarkThemeToggleService but not in AccordionTitleComponent, and I’m really stuck now.. There is also a BaseComponent which is parent of all of my components
Could anyone say me why am I getting an error in one case but not the other ? Also I’m moving the project to not use RxJs anymore (so using signal only)

I’m always having this error :

ERROR RuntimeError: NG0600: Writing to signals is not allowed in a `computed` or an `effect` by default. Use `allowSignalWrites` in the `CreateEffectOptions` to enable this inside effects.
    at throwInvalidWriteToSignalErrorFn (./node_modules/@angular/core/fesm2022/core.mjs:32179:15)
    at throwInvalidWriteToSignalError (./node_modules/@angular/core/fesm2022/primitives/signals.mjs:414:5)
    at signalUpdateFn (./node_modules/@angular/core/fesm2022/primitives/signals.mjs:460:9)
    at updateFn (./node_modules/@angular/core/fesm2022/core.mjs:17828:53)
    at set (./libs/flowbite-angular/src/lib/services/signal-store.service.ts:16:17)
    at onClick (./libs/flowbite-angular/src/lib/components/accordion/accordion-title.component.ts:70:43)
    at AccordionTitleComponent_Template (./libs/flowbite-angular/src/lib/components/accordion/accordion-title.component.html:10:3)
    at executeTemplate (./node_modules/@angular/core/fesm2022/core.mjs:11458:9)
    at refreshView (./node_modules/@angular/core/fesm2022/core.mjs:13000:13)
    at detectChangesInView (./node_modules/@angular/core/fesm2022/core.mjs:13231:9) {
  code: 600
}

BaseComponent :

import {
  Component,
  Injector,
  OnInit,
  effect,
  inject,
  signal,
} from '@angular/core';
import { FlowbiteClass } from '../common';

@Component({
  template: '',
  // eslint-disable-next-line @angular-eslint/no-host-metadata-property
  host: { '[class]': 'contentClasses()?.rootClass' },
})
export abstract class BaseComponent implements OnInit {
  protected injector = inject(Injector);
  protected contentClasses = signal<FlowbiteClass>({ rootClass: '' });

  public ngOnInit(): void {
    effect(
      () => {
        this.fetchClass();
      },
      { injector: this.injector, allowSignalWrites: true },
    );
  }

  /**
   * Function to load component's classes
   */
  protected abstract fetchClass(): void;
}

DarkThemeToggleComponent :

import * as properties from './dark-theme-toggle.theme';
import { BaseComponent } from '../base.component';
import { paramNotNull } from '../../utils/param.util';

import {
  AfterViewInit,
  Component,
  HostListener,
  afterNextRender,
  effect,
  inject,
  input,
  signal,
} from '@angular/core';
import { GlobalSignalStoreService } from '../../services/global-signal-store.service';
import { NgClass, NgIf } from '@angular/common';
import { ThemeState } from '../../services/state/theme.state';

@Component({
  standalone: true,
  imports: [NgIf, NgClass],
  selector: 'flowbite-dark-theme-toggle',
  templateUrl: './dark-theme-toggle.component.html',
})
export class DarkThemeToggleComponent
  extends BaseComponent
  implements AfterViewInit
{
  protected readonly themeGlobalSignalStoreService = inject<
    GlobalSignalStoreService<ThemeState>
  >(GlobalSignalStoreService<ThemeState>);

  protected override contentClasses = signal<properties.DarkThemeToggleClass>(
    properties.DarkThemeToggleClassInstance(),
  );

  //#region properties
  public customStyle = input<Partial<properties.DarkThemeToggleBaseTheme>>({});
  //#endregion

  //#region BaseComponent implementation
  protected override fetchClass(): void {
    if (paramNotNull()) {
      const propertyClass = properties.getClasses({
        customStyle: this.customStyle(),
      });

      this.contentClasses.set(propertyClass);
    }
  }
  //#endregion

  public ngAfterViewInit(): void {
    afterNextRender(
      () => {
        const localStorageTheme = localStorage.getItem('color-theme');

        if (
          localStorageTheme === 'dark' ||
          (!localStorageTheme &&
            window.matchMedia('(prefers-color-scheme: dark)').matches)
        ) {
          this.themeGlobalSignalStoreService.set('theme', 'dark');
          document.documentElement.classList.add('dark');
        } else {
          this.themeGlobalSignalStoreService.set('theme', 'light');
          document.documentElement.classList.remove('dark');
        }

        effect(
          () => {
            const theme = this.themeGlobalSignalStoreService.select('theme')();

            localStorage.setItem('color-theme', theme);
            theme === 'dark'
              ? document.documentElement.classList.add('dark')
              : document.documentElement.classList.remove('dark');
          },
          { injector: this.injector },
        );
      },
      { injector: this.injector },
    );
  }

  @HostListener('click')
  protected onClick() {
    if (this.themeGlobalSignalStoreService.select('theme')() === 'light')
      this.themeGlobalSignalStoreService.set('theme', 'dark');
    else this.themeGlobalSignalStoreService.set('theme', 'light');
  }
}

AccordionTitleComponent :

import * as properties from './accordion-title.theme';
import {
  AccordionPanelState,
  AccordionState,
} from '../../services/state/accordion.state';
import { BaseComponent } from '../base.component';
import { SignalStoreService } from '../../services/signal-store.service';
import { booleanToFlowbiteBoolean } from '../../utils/boolean.util';
import { paramNotNull } from '../../utils/param.util';

import { Component, HostListener, inject, input, signal } from '@angular/core';
import { NgClass } from '@angular/common';

@Component({
  standalone: true,
  imports: [NgClass],
  selector: 'flowbite-accordion-title',
  templateUrl: './accordion-title.component.html',
})
export class AccordionTitleComponent extends BaseComponent {
  protected accordionPanelSignalStoreService = inject<
    SignalStoreService<AccordionPanelState>
  >(SignalStoreService<AccordionPanelState>);
  protected accordionSignalStoreService = inject<
    SignalStoreService<AccordionState>
  >(SignalStoreService<AccordionState>);

  protected override contentClasses = signal<properties.AccordionTitleClass>(
    properties.AccordionTitleClassInstance(),
  );

  //#region properties
  protected customStyle = input<Partial<properties.AccordionTitleBaseTheme>>(
    {},
  );
  //#endregion

  //#region BaseComponent implementation
  protected override fetchClass(): void {
    if (
      paramNotNull(
        booleanToFlowbiteBoolean(
          this.accordionSignalStoreService.select('isFlush')(),
        ),
        booleanToFlowbiteBoolean(
          this.accordionPanelSignalStoreService.select('isOpen')(),
        ),
        this.customStyle(),
      )
    ) {
      const propertyClass = properties.getClass({
        customStyle: this.customStyle(),
        isFlush: booleanToFlowbiteBoolean(
          this.accordionSignalStoreService.select('isFlush')(),
        ),
        isOpen: booleanToFlowbiteBoolean(
          this.accordionPanelSignalStoreService.select('isOpen')(),
        ),
      });

      this.contentClasses.set(propertyClass);
    }
  }
  //#endregion

  @HostListener('click')
  protected onClick(): void {
    const isOpen = this.accordionPanelSignalStoreService.select('isOpen')();

    this.accordionPanelSignalStoreService.set('isOpen', !isOpen);
  }
}

Here is the SignalStoreService :

import { Injectable, Signal, computed, signal } from '@angular/core';

@Injectable()
export class SignalStoreService<T> {
  private _state = signal({} as T);

  public get state(): Signal<T> {
    return this._state.asReadonly();
  }

  public select<K extends keyof T>(key: K): Signal<T[K]> {
    return computed(() => this._state()[key]);
  }

  public set<K extends keyof T>(key: K, data: T[K]) {
    this._state.update((currentValue) => ({ ...currentValue, [key]: data }));
  }

  public setState(partialState: Partial<T>) {
    this._state.update((currentValue) => ({ ...currentValue, partialState }));
  }
}

The repo on GitHub : https://github.com/themesberg/flowbite-angular/tree/rework_documentation_front/libs/flowbite-angular

I tried with ContentClasses not being a signal property, but I need to make it update the template so I need it to be a Signal
Moving the update part in another function called by @HostListener’s function is not working

LEAVE A COMMENT