Netanel Basal
Netanel BasalFrontend Architect @ Datorama

Komponenty renderless w Angularze

Sprawdź, jak w Angularze stworzyć coś, co w React jest znane jako właściwości renderujące, a w Vue jako komponenty renderless.
5.10.20204 min
Komponenty renderless w Angularze

Pracuję zarówno z Angularem, jak i Reactem. Jestem też wielkim fanem Vue i Svelte i cały czas śledzę, co się z nimi dzieje. W tym artykule pokażę, jak w Angularze można osiągnąć to, co w Vue nazywa się komponentami renderless, a w React właściwościami renderującymi (render props). Jak sama nazwa wskazuje, renderless odnosi się do komponentów, które niczego nie renderują; ich jedynym zadaniem jest zapewnienie funkcji wielokrotnego użytku.

Weźmy na przykład przełącznik napisany w Vue i przekonwertujmy go na Angulara. Oto wersja Vue:

<toggle>
  <div slot-scope="{ on, setOn, setOff }">
    <button @click="click(setOn)">Blue pill</button>
    <button @click="click(setOff)">Red pill</button>
    <div>
      <span v-if="on">It's all a dream, go back to sleep.</span>
      <span v-else>
        I don't know how far the rabbit hole goes, 
        I'm not a rabbit, neither do I measure holes.
      </span>
    </div>
  </div>
</toggle>


Przełącznik ten w wersji renderless jest odpowiedzialny za udostępnianie API do przełączania widoku i nie ma znaczenia, jaka jest struktura lub styl widoku. Możemy osiągnąć to samo w Angularze, używając dyrektyw strukturalnych.

Dyrektywy strukturalne

Dyrektywa strukturalna zmienia układ DOM, dodając i usuwając elementy DOM (tj. view). Ma ona również funkcję dostarczania obiektu context, który jest dostępny dla każdego, kto go używa.

Utwórzmy dyrektywę strukturalną toggle, której API będzie dostępne za pośrednictwem właściwości context:

type Toggle = {
  on: boolean;
  setOn: Function;
  setOff: Function;
  toggle: Function;
}

@Directive({ selector: '[toggle]' })
export class ToggleDirective implements OnInit {
  on = true;

  @Input('toggleOn') initialState = true;

  constructor(private tpl: TemplateRef<{ $implicit: Toggle }>, 
              private vcr: ViewContainerRef) {
  }

  ngOnInit() {
    this.on = this.initialState;

    this.vcr.createEmbeddedView(this.tpl, {
      $implicit: {
        on: this.on,
        setOn: this.setOn,
        setOff: this.setOff,
        toggle: this.toggle,
      }
    });
  }

  setOn() { this.on = true }

  setOff() { this.on = false }

  toggle() { this.on = !this.on }
}


Tworzymy view i przekazujemy publiczne API, które chcemy wystawić za pośrednictwem context, który jest drugim parametrem createEmbeddedView.

Fajną rzeczą jest to, że TemplateRef przyjmuje typ ogólny, który służy jako kontekst. Nowoczesne IDE, takie jak Webstorm, będą w stanie obsłużyć to bez problemu. Użyjmy więc następującego:

<div *toggle="let controller; on: false">
 <button (click)="controller.setOn()">Blue pill</button>
 <button (click)="controller.setOff()">Red pill</button>

 <div>
   <span *ngIf="controller.on">...</span>
   <span *ngIf="!controller.on">...</span>
 </div>
</div>



Dzięki Webstorm! ?

ExportAs 

W drugim przypadku skorzystajmy z oficjalnego przykładu Reacta z właściwościami renderującymi. Właściwości renderujące umożliwiają nam współdzielenie stanu lub zachowania, hermetyzującego jeden komponent, z tymi, które również tego stanu potrzebują. Następujący komponent Reacta śledzi pozycję myszy w aplikacji webowej:

class Mouse extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
        {this.props.children(this.state)}
      </div>
    );
  }
}


Używamy właściwości renderujących children jako funkcji, która ujawnia stan komponentu każdemu konsumentowi. Technika ta sprawia, że zachowanie, które musimy udostępniać, jest niezwykle przenośne. Aby tego zachowania użyć, wyrenderuj <Mouse> z właściwością, która mówi mu, co ma renderować dla bieżącego (x, y) kursora:

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse>
          {mouse => (
            <p>The mouse position is {mouse.x}, {mouse.y}</p>
          )}
        </Mouse>
      </div>
    );
  }
}


Stwórzmy tę samą funkcję w Angularze:

@Component({
  selector: 'mouse',
  exportAs: 'mouse',
  template: `
    <div (mousemove)="handleMouseMove($event)">
      <ng-content></ng-content>
    </div>
  `
})
export class MouseComponent {
  private _state = { x: 0, y: 0 };
  get state() {
    return this._state;
  };

  handleMouseMove(event) {
    this._state = {
      x: event.clientX,
      y: event.clientY
    };
  }

}


MouseComponent nie przejmuje się zawartością. Udostępnia swoje API za pomocą właściwości exportAs, która mówi Angularowi, że możemy użyć tego komponentu w szablonie:

@Component({
  selector: 'mouse-tracker',
  template: `
    <mouse #mouse="mouse">
      <p>The mouse position is {{ mouse.state.x }}, {{ mouse.state.y }}</p>
    </mouse>
  `
})
export class MouseTrackerComponent {}


I to by było na tyle. Możemy też skorzystać z techniki exportAs, aby utworzyć przełącznik, który napisaliśmy wcześniej, zamiast używać dyrektywy strukturalnej.

Podsumowanie

Jeśli chodzi o używanie dyrektywy strukturalnej lub funkcji exportAs, to myślę, że nie ma dobrych i złych odpowiedzi. Korzyści z używania dyrektyw strukturalnych są takie, że możemy dzięki nim zdefiniować API, które ujawniamy w widoku oraz łatwo kontrolować, czy widok ma być renderowany, czy nie. Gdy nadal potrzebujemy jakiejś części widoku, jak w przykładzie z MouseComponent, możemy użyć komponentu i funkcji exportAs.


Oryginał tekstu w języku angieslkim możesz przeczytać tutaj.

<p>Loading...</p>