简体   繁体   中英

Angular proper way of displaying model calculated data on template

Considering a Person model, for example:

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

and a calculated/derived data from it, such as fullName that is `${firstName} ${lastName}` (this is just an example because the calculation could be more complex and heavy).

What is the proper way to display the calculated data on components template? Considering that:

  • I want to respect DRY as much as possible, I don't want duplicate my model or write many times on templates {{ person.firstName }} {{ person.lastName }} ;
  • It is not viable to use getters or methods on template, due to performance issue, even using OnPush change detection strategy;
  • The calculation logic must stay in the model, because I need to use it somewhere in the TS code, so I don't want to use pure pipes, or the proposed component computed properties (if at some time will be implemented) or some workaround to get a similar behavior.

I have considered to use an immutable approach to the Person model, and calculate derived properties only in the constructor:

class Person {
  firstName: string;
  lastName: string;
  readonly fullName: string;
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.fullName = `${firstName} ${lastName}`;
  }
}

Obviously with this approach I can't do person.firstName = 'Changed' to also update fullName property, but I have to do person = new Person('Changed', person.lastName) .
I've considered to apply readonly also to firstName and lastName to ensure the immutability pattern, but in this way I'm not able to use Person model in another template context where, for example, I need to bind firstName and lastName to some inputs models.

I'm really interested to know what is the standard/proper/best practice way to properly handle this common situation.

I would consider Using properties (get)


class Person {
 firstName: string;
 lastName: string;
 
 get fullName(): string { return `${this.firstName} ${this.lastName}`;}

 constructor(firstName: string, lastName: string) {
   this.firstName = firstName;
   this.lastName = lastName;
 }
}

EDIT

To optimize for often occuring changes and reduce the recalculation you can use a Subject and pipe it with debounceTime.


class Person{

  private _firstName: string;
  private _lastName: string;
  private _fullName: string;
  private _debounceFirstname = new Subject<string>();
  private _debounceLastname = new Subject<string>();

  constructor(firstName: string, lastName: string) {
    this._firstName = firstName;
    this._lastName = lastName;
    this._debounceFirstname
           .pipe(debounceTime(300)).subscribe((value: string) => this.firstName = value);
    this._debounceLastname
           .pipe(debounceTime(300)).subscribe((value: string) => this.lastName = value);
    this.changeCalculation();
  }

  get firstName(): string {
    return this._firstName;
  }

  set firstName(value: string) {
    this._firstName = value;
    this.changeCalculation();
  }
  get lastName(): string {
    return this._lastName;
  }

  set lastName(value: string) {
    this._lastName = value;
    this.changeCalculation();
  }

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

  private changeCalculation(): void {
      this._fullName= `${this.firstName} ${this.lastName}`;
  }

}

I see that you want to avoid the getters and methods, but I am not entirely convinced that the performance downsides of such approach are so expensive. In general, if those getters and methods don't do some heavy work, I don't think you should drop them. Of course, as in your case, these operations can be more complex.

My subjective approach to handle this scenario would be to utilize getters and setters, while avoiding the fullName calculations as much as possible:

class Person {
  _firstName: string;
  _lastName: string;
  _fullName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
    this._fullName = `${firstName} ${lastName}`; // First setting of the fullName
  }

  get firstName() {
    return this._firstName;
  }

  set firstName(newFirstName: string) {
    if (newFirstName !== this._firstName) {
      // Update the full name
      this._fullName = `${newFirstName} ${this._lastName}`;
      this._firstName = newFirstName;
    }
  }

  get lastName() {
    return this._lastName;
  }

  set lastName(newLastName: string) {
    if (newLastName !== this._lastName) {
      // Update the full name
      this._fullName = `${this._firstName} ${newLastName}`;
      this._lastName = newLastName;
    }
  }

  get fullName() {
    // Memoizing the full name
    if (!this._fullName) {
      // The full name is not going to be recalculated every time
      this._fullName = `${this._firstName} ${this._lastName}`;
    }
    return this._fullName;
  }
}

This is a reactive-ish example. Some people think the use of BehaviorSubject is a bit of anti-pattern in RXJS, but that is a different discussion.

Template Code - Use an async pipe to get values out of an observable and to manage the subscriptions.

<div>{{myPerson.fullName$ | async}}</div>

Model Code - Define a behavior subject for first and last name. Use combinelatest to detect changes to first or last name.

class Person {
  private firstNameSubject: BehaviorSubject<string>;
  private lastNameSubject: BehaviorSubject<string>;
  fullName$: Observable<string>;
  constructor(firstName: string, lastName: string) {
    this.firstNameSubject = new BehaviorSubject(firstName);
    this.lastNameSubject = new BehaviorSubject(lastName);
    this.fullName$ = combineLatest([firstName, lastName]).pipe(
        map(([first, last]) => computeFullName(first, last)),
        shareReplay(1) //make our stream replayable to so we don't need to recompute the value per subscription
    );
  }
  setfirstName(newFirstName: string) {
    this.firstNameSubject.next(newFirstName);
  }
  setlastName(newLastName: string) {
    this.lastNameSubject.next(newLastName);
  }
  getFirstName() {
    return this.firstNameSubject.value;
  }
  getLastName() {
    return this.lastNameSubject.value;
  }

}
export function computeFullName(firstName: string, lastName: string) {
    return `${firstName} ${lastName}`;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM