Anota una vez: AngularDart

Introduciendo herencia de componentes

Un colibrí de origami azul polinizando una flor naranja.

AngularDart permite a los desarrolladores crear componentes y directivas anotando definiciones de clase con metadatos. Estos metadatos instruyen al compilador para generar el código necesario para la inyección de dependencias, construcción de plantillas, enlace de datos y más.

dart
import 'package:angular/angular.dart';

@Component(
  selector: 'greet',
  template: '<p>Hello !</p>',
)
class GreetComponent {
  String name;
}

Hasta hace poco, el compilador de AngularDart no procesaba los metadatos en los supertipos. Esto significaba que compartir funcionalidad común entre diferentes componentes y directivas vía herencia requería redeclarar todos los metadatos en cada clase.

Veamos cómo se veía este patrón.

A lo largo de este artículo, se revisitará el siguiente ejemplo para demostrar los beneficios de la herencia de metadatos. Este es un ejemplo simplificado extraído de los componentes oficiales de Material Design para AngularDart utilizados en Google.

dart
import 'dart:async';
import 'dart:html';

import 'package:angular/angular.dart';

@Directive(selector: '[my-button]')
class MyButtonDirective {
  final StreamController<UIEvent> _trigger = new StreamController<UIEvent>();

  @Input()
  bool disabled = false;

  @Output()
  Stream<UIEvent> get trigger => _trigger.stream;

  @HostListener('click', const [r'$event'])
  void onClick(MouseEvent event) {
    if (disabled) return;
    _trigger.add(event);
  }

  @HostListener('keypress', const [r'$event'])
  void onKeyPress(KeyboardEvent event) {
    if (disabled) return;
    if (event.keyCode == KeyCode.ENTER) {
      _trigger.add(event);
    }
  }
}

Aquí tenemos una directiva simple que añade funcionalidad de botón a su elemento host. Si se hace clic en el elemento host, o se enfoca cuando se presiona la tecla enter, emitirá un evento a través del output trigger. También puede deshabilitarse para evitar emitir eventos. Esta lógica puede reutilizarse para crear un componente de botón usando herencia.

dart
import 'package:angular/angular.dart';

import 'my_button_directive.dart';

@Component(
  selector: 'my-button',
  template: '<ng-content></ng-content>',
  inputs: const ['disabled'],
  outputs: const ['trigger'],
  host: const {
    '(click)': r'onClick($event)',
    '(keypress)': r'onKeyPress($event)',
  },
)
class MyButtonComponent extends MyButtonDirective {}

Ahora tenemos un componente que hereda el comportamiento de botón de la directiva, pero ¿notas la necesidad de redeclarar el input, los outputs y los host listeners en la anotación del componente derivado?

¡A partir de la versión 4.0, redeclarar metadatos en tipos derivados ya no es necesario!

dart
import 'package:angular/angular.dart';

import 'my_button_directive.dart';

@Component(
  selector: 'my-button',
  template: '<ng-content></ng-content>',
)
class MyButtonComponent extends MyButtonDirective {}

Ahora MyButtonComponent tiene implícitamente los mismos inputs, outputs y host listeners que MyButtonDirective, similar a cómo una clase hereda normalmente propiedades y métodos. Ahora que has visto un ejemplo simple — y esperamos que convincente — veamos qué es exactamente heredable y cómo funciona.

¿Qué es heredable?

#

Las siguientes anotaciones ahora se heredan de todos los supertipos, incluyendo superclases, interfaces y mixins.

  • @ContentChild / @ContentChildren

  • @HostBinding

  • @HostListener

  • @Input

  • @Output

  • @ViewChild / @ViewChildren

Esto significa que ya no es necesario redeclarar ninguna de las anotaciones anteriores en una clase derivada.

¿Qué no es heredable?

#

Las plantillas, estilos y otros parámetros de @Directive no se heredan. Solo los metadatos que están vinculados a una propiedad o método se heredan. Esta decisión se tomó porque los tipos derivados pueden sobrescribir accessores y métodos, referenciando su implementación original vía super, mientras que no existe tal mecanismo para los valores vinculados a otros metadatos.

Los metadatos heredados son inmutables

#

Sigue siendo válido redeclarar anotaciones en tipos derivados; sin embargo, modificar los metadatos heredados o vincularlos a una propiedad diferente no está permitido.

dart
import 'package:angular/angular.dart';

import 'my_button_directive.dart';

@Directive(selector: '[illegal-button]')
class IllegalButtonDirective extends MyButtonDirective {
  @Input('notEnabled') // Changing the binding name causes a compile error.
  set disabled(bool value) {
    super.disabled = value;
  }
}

Esta restricción podría levantarse en el futuro, pero por ahora es necesaria para simplificar las herramientas y asegurar que los componentes y directivas sean fácilmente sustituibles.

Tipos base de metadatos

#

Una consecuencia interesante surge como resultado de la herencia de metadatos: las clases que no son directivas ni componentes en sí mismas, ahora pueden anotarse para servir como tipos base o mixins para otros. Desarrollemos además nuestro ejemplo para demostrarlo.

Hay un problema con la implementación actual del componente de botón: el botón no es enfocable. Esto significa que no es accesible y no puede activarse mediante el listener de pulsaciones de teclas. Este problema también está presente en la directiva de botón si su elemento host no es enfocable. Podemos resolver este problema haciendo que la directiva de botón establezca la propiedad tabindex en el elemento host.

MyButtonDirective podría implementar esto directamente, pero imaginemos que tenemos otros componentes que también querrán comportamiento enfocable. En su lugar, crearemos una clase abstracta que pueda ser extendida o mezclada por directivas o componentes.

dart
import 'package:angular/angular.dart';

abstract class HasTabIndex {
  bool get disabled;

  @HostBinding('tabindex')
  String get tabIndex => disabled ? null : '0';
}

Ahora podemos simplemente extender o mezclar HasTabIndex para hacer que la directiva de botón — y consecuentemente el componente derivado — sean enfocables.

dart
@Directive(selector: '[my-button]')
class MyButtonDirective extends HasTabIndex {
  ...
}

Conclusión

#

Hemos visto cómo los metadatos heredables reducen la duplicación de código entre componentes y directivas con funcionalidad compartida. Además, debería mejorar la legibilidad al permitir que los metadatos vivan junto a su campo o método asociado. Esperamos que encuentres esta característica útil en el desarrollo y mantenimiento de tus propios proyectos.

Más de Dart