Search code examples
typescriptserializationangular-signalsangular18

Serialising a model with signals in Angular 18


I'm trying to serialise a model object to local storage (or IndexedDB), however it contains signals.

// An example of the model I'm dealing with
export class ExampleModel {
    canSerialise = "";
    cantSerialise = signal("");
}

// What I'd like to be able to do
let exampleModel = new ExampleModel();
localStorage.setItem("example", JSON.stringify(exampleModel));

However this will not serialise cantSerialise as signals are technically functions. I'd like to avoid manually writing serialisation functions for all models that just have a signal in them.

  • Tried simply serialising using JSON.stringify():
    • Desired outcome: '{"canSerialise":"","cantSerialise":X}' where X can be anything as long as it serialises back into the original value as a signal (in this case signal(""))
    • Results (as I feared/expected) in '{"canSerialise":""}'
  • Thought about using IndexedDB but it won't be able to serialise signals either since the structured cloning algorithms has the same limitation of nog serialising functions.

EDIT: I should clarify the goals is for the model to be serialised and that said model happens to have signals in it, I'm given the model and can't change it. A the same time i would like to stress that deserialisation does need to result in having signals in the same places as before serialisation (e.g. cantSerialise should be wrapped in a signal during deserialisation but canSerialise should not) as the code dealing with the model (which again I don't have control over) expects those signals. The signals don't need to be the exact same signals per se i.e. just storing their value during serialisation and wrapping them in a new signal during deserialisation is acceptable.


Solution

  • This is not perfect code, but you can write a custom toString method that executes the signal before writing to an object. Then we can call JSON.stringify to convert the object which contains only the values into a string.

    Please ignore my TypeScript mistakes, since it's the best I can do; it might look ugly but it gets the job done.

    export class ToStringConverter {
      toString() {
        let output: any = {};
        const that: ToStringConverter = this as never as ToStringConverter;
        for (let key in that) {
          if (that!.hasOwnProperty(key)) {
            const lookupKey: keyof ToStringConverter =
              key as keyof ToStringConverter;
            if ((that[lookupKey] as any) instanceof Function) {
              output[lookupKey] = (that[lookupKey] as Function)() as any;
            } else {
              output[lookupKey] = that[lookupKey];
            }
          }
        }
        return JSON.stringify(output);
      }
    }
    

    Full code:

    import { Component, signal } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    
    export class ToStringConverter {
      toString() {
        let output: any = {};
        const that: ToStringConverter = this as never as ToStringConverter;
        for (let key in that) {
          if (that!.hasOwnProperty(key)) {
            const lookupKey: keyof ToStringConverter =
              key as keyof ToStringConverter;
            if ((that[lookupKey] as any) instanceof Function) {
              output[lookupKey] = (that[lookupKey] as Function)() as any;
            } else {
              output[lookupKey] = that[lookupKey];
            }
          }
        }
        return JSON.stringify(output);
      }
    }
    
    // An example of the model I'm dealing with
    export class ExampleModel extends ToStringConverter {
      canSerialise = '';
      cantSerialise = signal('');
    
      constructor(canSerialize = '', cantSerialize = '') {
        super();
        this.canSerialise = canSerialize;
        this.cantSerialise = signal(cantSerialize);
      }
    }
    
    @Component({
      selector: 'app-root',
      standalone: true,
      template: `
        <h1>Hello from {{ name }}!</h1>
        <a target="_blank" href="https://angular.dev/overview">
          Learn more about Angular
        </a>
      `,
    })
    export class App {
      name = 'Angular';
    
      ngOnInit() {
        // What I'd like to be able to do
        let exampleModel = new ExampleModel('working', 'also working');
        localStorage.setItem('example', exampleModel.toString());
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo