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 :
<code>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>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
}
</code>
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 { FlowbiteClass } from '../common';
// 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 {
{ injector: this.injector, allowSignalWrites: true },
* Function to load component's classes
protected abstract fetchClass(): void;
<code>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;
}
</code>
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 :
<code>import * as properties from './dark-theme-toggle.theme';
import { BaseComponent } from '../base.component';
import { paramNotNull } from '../../utils/param.util';
import { GlobalSignalStoreService } from '../../services/global-signal-store.service';
import { NgClass, NgIf } from '@angular/common';
import { ThemeState } from '../../services/state/theme.state';
imports: [NgIf, NgClass],
selector: 'flowbite-dark-theme-toggle',
templateUrl: './dark-theme-toggle.component.html',
export class DarkThemeToggleComponent
protected readonly themeGlobalSignalStoreService = inject<
GlobalSignalStoreService<ThemeState>
>(GlobalSignalStoreService<ThemeState>);
protected override contentClasses = signal<properties.DarkThemeToggleClass>(
properties.DarkThemeToggleClassInstance(),
public customStyle = input<Partial<properties.DarkThemeToggleBaseTheme>>({});
//#region BaseComponent implementation
protected override fetchClass(): void {
const propertyClass = properties.getClasses({
customStyle: this.customStyle(),
this.contentClasses.set(propertyClass);
public ngAfterViewInit(): void {
const localStorageTheme = localStorage.getItem('color-theme');
localStorageTheme === 'dark' ||
window.matchMedia('(prefers-color-scheme: dark)').matches)
this.themeGlobalSignalStoreService.set('theme', 'dark');
document.documentElement.classList.add('dark');
this.themeGlobalSignalStoreService.set('theme', 'light');
document.documentElement.classList.remove('dark');
const theme = this.themeGlobalSignalStoreService.select('theme')();
localStorage.setItem('color-theme', theme);
? document.documentElement.classList.add('dark')
: document.documentElement.classList.remove('dark');
{ injector: this.injector },
{ injector: this.injector },
if (this.themeGlobalSignalStoreService.select('theme')() === 'light')
this.themeGlobalSignalStoreService.set('theme', 'dark');
else this.themeGlobalSignalStoreService.set('theme', 'light');
<code>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');
}
}
</code>
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 :
<code>import * as properties from './accordion-title.theme';
} 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';
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(),
protected customStyle = input<Partial<properties.AccordionTitleBaseTheme>>(
//#region BaseComponent implementation
protected override fetchClass(): void {
booleanToFlowbiteBoolean(
this.accordionSignalStoreService.select('isFlush')(),
booleanToFlowbiteBoolean(
this.accordionPanelSignalStoreService.select('isOpen')(),
const propertyClass = properties.getClass({
customStyle: this.customStyle(),
isFlush: booleanToFlowbiteBoolean(
this.accordionSignalStoreService.select('isFlush')(),
isOpen: booleanToFlowbiteBoolean(
this.accordionPanelSignalStoreService.select('isOpen')(),
this.contentClasses.set(propertyClass);
protected onClick(): void {
const isOpen = this.accordionPanelSignalStoreService.select('isOpen')();
this.accordionPanelSignalStoreService.set('isOpen', !isOpen);
<code>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);
}
}
</code>
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 :
<code>import { Injectable, Signal, computed, signal } from '@angular/core';
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 }));
<code>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 }));
}
}
</code>
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