How do you use computed/calculated properties in Angular?
Asked Answered
F

3

10

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

Financier answered 1/7, 2019 at 18:29 Comment(5)
I would go with option 2 where I would understand personService as entity repository not sure what you mean by "Our personService returns object without this getter at all." can you elaborate?Typewritten
@Typewritten have a look my small example stackblitz.com/edit/angular-cwgqhu. Also problem explains here: #49517376Financier
yes you didn't create actual object of that model you just told typescript to assume that it should be that kind of return value you would need to initialize Person model with data returned by server so it would require to have constructor that can convert server response into its fieldsTypewritten
@Typewritten ok. then how can you go with option 2 ? (you wrote in the first comment)Financier
I have added draft how can you approach it but it may require some edge case modifications and also didn't actually run the code so that may need some fixesTypewritten
T
2

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.

Typewritten answered 2/7, 2019 at 20:53 Comment(0)
C
0

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


Chargeable answered 1/7, 2019 at 19:20 Comment(0)
N
0

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.

Nne answered 13/11, 2019 at 13:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.