loading...

Input-Set pattern for Widgets in Angular

michaeljota profile image Michael De Abreu 惻3 min read

Intro

If you read my last post, you should know what a Widget is, if you haven't already, do it now, I'll await.

await Reader.read('Widgets and components');

Now you know what Widgets should be. I tried to keep it simple for the first implementation, but I'm really turning my head around this. The thing is, React is a JavaScript pure framework and with JSX allow us to do things that we can't made with plain HTML templates, nevertheless, Typescript allow us for things that JavaScript does not, at the very least, it seems to allow that. Like private properties. With that in mind, I'll try to explain why you should use private properties inside a Widget and how to manage the data entry in a better way.

The situation

Following the comparative with React from the last post, when the props are send to a component, most of the the time we are getting it following the destructive pattern, allowing to do things like:

interface IUser {
  firstname: string;
  lastname: string;
  age: number;
}

export function usersList({users = []} : { IUser[] }) {
  return (<ul>
    users.map(user => userDetails(user));
  </ul>)
}

function userDetails({firstname, lastname, age}: IUser) {
    return (<li><span>{firstname} {lastname}</span>have {age} years old</li>);
}

I think this looks good, because it allow us to be explicit about what we want to show, and how to show it.

In Angular, we just @Input the data that we want to get from the outside.

// user.ts
interface IUser {
  firstname: string;
  lastname: string;
  age: number;
}

// user-list.widget.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-user-list',
  template: `
  <ul>
    <li *ngFor="let user of users">
      <my-user-details [user]=user><my-user-details>
    </li>
  </ul>
  `
})
export class UserListWidget {
  @Input()
  public users: IUser[];
}

// user-details.widget.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-user-details',
  template: 
    `<span>{{user.firstname}} {{user.lastname}}</span> has {{user.age}} years old`,
})
export class UserDetailsWidget {
  @Input()
  public user: IUser;
}

This is right, as Widgets are suppose to display data, and they should only concern should be how to display that data. But we need a better way to organize how we show that data correctly in the template.

I like the approach from React in that sense. Don't get me wrong, I love Angular. But JSX allow us to do something nicer than dotter properties, destructive assignment.

Solution proposal

Now that is being clearer that React have a somewhat advantage above us, we as Angular developers need to propose a solution to be even, using features that are being bringed by Typescript, private properties. I know that Typescript is compiled to JavaScript, and JavaScript doesn't have such things, but, we'll do as we don't care.
Also, we will use a ES6 set feature. This allow us to have properties that can be set, but cannot be read.

Introducing @input set

import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-user-details',
  template: 
    `<span>{{firstname}} {{lastname}}</span> has {{age}} years old`,
})
export class UserDetailsWidget {
  private _user: IUser;

  @Input()
  public set user(value: User): void {
    this._user = value;
  }
}

OK. We learn we can set to private properties, but that does not quite allow us to show the properties in the template. Technically JavaScript only have properties, so we could bind to the private property, but we should never bind a private property to the template, as the AOT compiler would throw.

Introducing template get

@Component({
  selector: 'my-user-details',
  template: 
  `<span>{{firstname}} {{lastname}}</span> has {{age}} years old`,
})
export class UserDetailsWidget {
  private _user: IUser;
  @Input()
  public set user(value: IUser) {
    this._user = value;
  }

  public get firstname(): string {
    return this._user.firstname;
  }
  public get lastname(): string {
    return this._user.lastname;
  }
  public get age(): age {
    return this._user.age;
  }
}

This actually works as expected. Allowing a better control over the data that are being displayed in the template, and how to expose it. Also, it would even allow us to do certain things, to maintain the code in a better way.

@Component({
  selector: 'my-user-details',
  template: 
  `<span>{{fullname}}</span> has {{age}} years old`,
})
export class UserDetailsWidget {
  private _user: IUser;
  @Input()
  public set user(value: IUser) {
    this._user = value;
  }

  public get fullname(): string {
    return `${this._user.firstname} ${this._user.lastname}`;
  }
  public get age(): number {
    return this._user.age;
  }
}

However, template get as showed here have certain issues, but I'll explain everything about those issues properly in other post.

Example

For this, I made a little Plunker that expose the use case. Here

Thanks you

As always, thank you for reading this long. I hope you like it, and I am really looking forward to read your thoughts in the comments section.

Discussion

pic
Editor guide