Anota una vez: AngularDart
Introduciendo herencia de componentes
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.
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.
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.
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!
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.
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.
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.
@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
Historia del interop con JS en Dart
El soporte para Wasm acaba de llegar al beta actual de Flutter, gracias a un emocionante hito de interop con JavaScript alcanzado en Dart 3.3. Para…
Dart DevTools: Analizando el rendimiento de la aplicación con el CPU Profiler
Ya seas un desarrollador de backend escribiendo una herramienta de línea de comandos con Dart, o un ingeniero de UX construyendo aplicaciones con Flutter, el rendimiento del programa es crítico para el éxito de tu proyecto. Las herramientas de línea de comandos deberían minimizar la latencia y las apps deberían ser responsivas y ágiles sin fotogramas perdidos. Como desarrolladores, hacemos todo lo posible para escribir código performante pero a veces no está claro por qué nuestro código no funciona tan bien como esperaríamos.