How do you use computed/calculated properties in Angular?

Viewed 5282

I am tormented by the question: where I should locate my calculated properties in angular project?

For example: I have model, service to get model and component to show model.

person.model.ts:

export class Person {
  firstName: string;
  lastName: string;
}

person.service.ts:

export class PersonService {

  // inject http: HttpClient

  get(id) {
   return this.http.get<Person>(`api-endpoint/person/${id}`);
  }
}

person.component.ts

@Component({
  selector: 'app',
  template: `
   <div>
    <input [value]='person.firstName'>
    <input [value]='person.lastName'>
   </div>
`,
  providers:  [ PersonService ]
})
export class AppComponent {
  person: Person;

  // inject personService: PersonService

  ngOnInit() {
   personService.get(1).subscribe(p => this.person = p);
  }
}

And now I need fullName to show it into template under my input fields.

Option 1. If you google 'calculated properties angular' you will most likely find examples with calculated properties in the component iteself.

@Component({
  selector: 'app',
  template: `
   <div>
    <input [value]='person.firstName'>
    <input [value]='person.lastName'>
    <span>{{ fullName }}</span>
   </div>
`,
  providers:  [ PersonService ]
})
export class AppComponent {
  person: Person;
  get fullName() {
    return `${this.person.firstName} ${this.person.lastName}`
  }
  // inject personService: PersonService

  ngOnInit() {
   personService.get(1).subscribe(p => this.person = p);
  }
}

But is this the right place for such code? What if we would like to reuse this property in other components, services and etc?

Option 2. I personally want to extend person.model.ts.

export class Person {
  firstName: string;
  lastName: string;
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`
  }
}
@Component({
  selector: 'app',
  template: `
   <div>
    <input [value]='person.firstName'>
    <input [value]='person.lastName'>
    <span>{{ person.fullName }}</span>
   </div>
`,
  providers:  [ PersonService ]
})
export class AppComponent {
  person: Person;
  // inject personService: PersonService

  ngOnInit() {
   personService.get(1).subscribe(p => this.person = p);
  }
}

But then we face with another problem. Our personService returns object without this getter at all.

So what should I do? Do I need to create new instance of person.model.ts and then assign our response to it or maybe I need another model at all, something like person.view-model.ts?

Thanks for your time :D

3 Answers

Base solution

So for option 2 you need to envelop data coming from the server into a model. Here is one of possible ways of doing this:

person.model.ts

export class Person {
  firstName: string;
  lastName: string;

  constructor(obj) {
    Object.assign(this, obj);
  }

  get fullName(): string {
    return this.firstName + this.lastName;
  }
}

And when you implement this or with some additional guarding logic (check extended solution) you can do this in your service:

person.service.ts

export class PersonService {
  ...
  get(id): Observable<Person> {
    return this.http.get<Partial<Person>>(`api-endpoint/person/${id}`).pipe(
      map((response: Partial<Person>) => new Person(response)),
    );
  }
}

Partial<Person> type because it wont have some dynamic properties

Extended solution

Although this basic solution is vulnerable to run time errors. For example, if initialization object has some fields conflicting with class definition and also it doesn't care if obj has some additional fields so you may want to add some more logic like for example:

filtered-object.helper.ts

import { keys } from 'ts-transformer-keys'; // enables to take out keys from interface

export function filteredObjec<T>(obj) {
  const allowed = keys<T>();
  return Object.keys(obj)
    .filter(key => allowed.includes(key))
    .reduce((obj, key) => {
      obj[key] = raw[key];
      return obj;
    }, {});
}

bare in mind this requires to install ts-transformer-keys

person.model.ts

import { filteredObjec } from 'path/to/filtered-object.helper';
...
  constructor(obj) {
    Object.assign(this, filteredObjec<Person>(obj));
  }

Alternatively, you could create your own decorators for describing fields I won't provide an example here as this would be quite excessive.

I'll add a pattern that I find very useful. I personally like to keep a separation of concerns - i.e keep all Person related computed properties in the PersonService and consume those properties directly from the service. Especially if you are using them elsewhere. In your PersonService I would add a BehaviourSubject that you can bind to in your templates elsewhere:

// person.serice.ts

private readonly _fullName: BehaviorSubject<string> = new BehaviorSubject('');
public readonly fullName$ = this._fullName.asObservable();

public get fullName(): string {
    return this._fullName.getValue();
}

public set fullName(p: Person): string {
    this._fullName.next(p.firstName + p.lastName);
}

// inject http: HttpClient

get(id) {
   return this.http.get<Person>(`api-endpoint/person/${id}`)
       .pipe(tap((p: Person) => this.fullName = p);
}

And in your component:

// person.component.ts

@Component({
  selector: 'app',
  template: `
   <div>
    <input [value]='person.firstName'>
    <input [value]='person.lastName'>
    <span>{{ fullName$ | async }}</span>
   </div>
`,
  providers:  [ PersonService ]
})
export class AppComponent {
  person: Person;
  fullName$: Observable<string>;

  // inject personService: PersonService

  ngOnInit() {
   this.fullName$ = this.personService.fullName$;
   this.personService.get(1).subscribe(p => this.person = p);
  }
}


I have been using this npm module with success for a long time now:

https://www.npmjs.com/package/class-transformer

If you're ok with having classes for your models, it's a life-saver and it can handle deep nesting of classes, and lots of other non-trivial cases.

Unfortunately, our team decided to use interfaces for models, so I'm now about to remove all this code that uses that module.

Related