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:
{{ person.firstName }} {{ person.lastName }}
;OnPush
change detection strategy; 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.