¡Hay una nueva API de componentes en Octane! En esta sección, nos centraremos en las diferencias entre el nuevo estilo, conocido como componentes Glimmer, y los componentes clásicos, y cómo actualizarlos. Los componentes “clásicos” se refieren a componentes de estilo antiguo que no utilizan clases nativas.

Estos nuevos tipos de componentes exigir sintaxis de clase nativa. Puedes definir uno como este:

import Component from '@glimmer/component';
import  tracked  from '@glimmer/tracking';

export default class TodoComponent extends Component 
  @tracked completed;

Puede notar que la importación proviene de un paquete llamado @glimmer, no @ember. Detrás de escena, el motor de renderizado de Ember proviene de Glimmer, y ahora los componentes también. Sin embargo, Glimmer es una integración de bajo nivel con Ember, y no necesita preocuparse por aprenderlo por separado.

Beneficios de los componentes Glimmer

Los componentes de Glimmer tienen enormes beneficios:

  • Estos nuevos componentes le brindan todos los beneficios descritos en Clases nativas arriba
  • No se extienden desde EmberObject en absoluto, lo que significa que no necesitan EmberObject API, como reopenClass, extend. Puede utilizar de forma segura constructor para todo el código de configuración.
  • Los ganchos del ciclo de vida se simplifican enormemente y son más fáciles de usar
  • No tienen ese elemento HTML envolvente que se interpuso en el estilo y el diseño CSS

Los argumentos también tienen un espacio de nombres en this.args dentro de los componentes de Glimmer, que es un objeto inmutable. Esto significa que:

  • Está claro cuando accede a los argumentos pasados ​​al componente y cuando accede a los campos y propiedades del propio componente.
  • Los argumentos siempre se refieren al valor original que se pasó, por lo que no tiene que rastrear código confuso en ganchos o definiciones de propiedades calculadas que modifiquen el valor del argumento.
  • No hay un enlace de datos bidireccional confuso para los argumentos a través de la clase de componente, los datos solo pueden fluir en una dirección.

Acostumbrarse a los componentes Glimmer

Ciclo de vida y propiedades

Estos componentes tienen 2 ganchos de ciclo de vida:

  • constructor
  • willDestroy

Estos se pueden usar para configurar la clase y derribarla, respectivamente. Otros ganchos del ciclo de vida, como didInsertElement y didUpdate no tengo equivalentes. En su lugar, deberías usar modificadores para llenar sus casos de uso. Estos se comentan con más detalle más adelante.

Los componentes también tienen 3 propiedades:

  • args – los argumentos que recibe el componente cuando se invoca. Estos se pasan y se asignan en el constructor, por lo que están disponibles para cualquier código de configuración que se necesite.
  • isDestroying – Se establece en verdadero cuando el componente se ha marcado para su destrucción.
  • isDestroyed – Se establece en verdadero cuando el componente se ha destruido por completo.

HTML externo

Estos componentes no tienen un elemento de envoltura. Esto se conoce como semántica HTML externa, y significa que todo lo que ve en la plantilla es lo que obtiene en el DOM renderizado final:

!-- template.hbs --
Hello, this.worldName!

Hello, Earth!

Esto significa que ya no tiene que personalizar su componente utilizando ninguna de las siguientes API:

  • tagName
  • classNames
  • classNameBindings
  • attributeBindings

En su lugar, puede hacer esto directamente en su plantilla. A continuación, se muestran algunos ejemplos de antes y después de cada API, convertidos a partir de componentes clásicos:

  • tagName

    Antes:

    import Component from '@ember/component';
    
    export default Component.extend(
    tagName: 'button',
    text: 'Hello, world!',
    
    click() 
      console.log('Hello, world!');
    
    );
    
    this.text
    

    Después:

    import Component from '@glimmer/component';
    import  action  from '@ember/object';
    
    export default class HelloButtonComponent extends Component 
    text = 'Hello, world!';
    
    @action
    sayHello() 
      console.log('Hello, world!');
    
    
    
    
    
  • classNames

    Antes:

    import Component from '@ember/component';
    
    export default Component.extend(
    classNames: ['hello-world'],
    text: 'Hello, world!'
    );
    
    this.text
    

    Después:

    import Component from '@glimmer/component';
    
    export default class HelloComponent extends Component 
    text = 'Hello, world!';
    
    
    this.text
  • classNameBindings

    Antes:

    import Component from '@ember/component';
    
    export default Component.extend(
    classNameBindings: ['darkMode:dark-mode'],
    darkMode: false,
    text: 'Hello, world!'
    );
    
    this.text
    

    Después:

    import Component from '@glimmer/component';
    import  tracked  from '@glimmer/tracking';
    
    export default class HelloComponent extends Component 
    text = 'Hello, world!';
    @tracked darkMode = false;
    
    
    this.text
  • attributeBindings

    Antes:

    import Component from '@ember/component';
    
    export default Component.extend(
    attributeBindings: ['role'],
    role: 'button',
    text: 'Hello, world!'
    );
    
    this.text
    

    Después:

    import Component from '@glimmer/component';
    
    export default class HelloComponent extends Component 
    text = 'Hello, world!';
    role = 'button';
    
    
    this.text

En resumen, el nuevo modelo mental es que el elemento de “envoltura” es como cualquier otro elemento en su plantilla, y usted interactúa con él exactamente de la misma manera. Esto significa que al convertir un componente clásico, deberá agregar el elemento de envoltura que estaba allí anteriormente a la plantilla (a menos que fuera un componente sin etiquetas, por ejemplo tagName: '').

...attributes

Cuando pasa atributos HTML estándar a un componente (como class, alt, role, etc.), debe indicarle a la plantilla dónde colocarlos. Recuerde, ¡ya no hay ningún elemento de envoltura! La forma en que muestra dónde aplicar los atributos es utilizando ...attributes en la plantilla.

Por ejemplo, aquí pasamos un class a un componente:


Y en ese componente, podemos aplicar la clase al párrafo usando ...attributes:

!--
  The paragraph gets the attributes, and not the h1
--

Hello, world!

Lorem Ipsum...

Los atributos también se pueden aplicar a varios elementos:

!-- Both elements get the attributes --

Hello, world!

Lorem Ipsum...

Puede aplicar ...attributes a elementos que también tienen atributos explícitos. Si ...attributes proviene después otro atributo, entonces será posible que el usuario los anule:

...

Finalmente, si no aplica ...attributes para alguna elementos, entonces Ember arrojará un error si alguien intenta usar atributos al invocar su componente:

!-- components/uncustomizable-button.hbs --

!-- This throws an error --

Los atributos también están disponibles para componentes clásicos y ...attributes se aplica automáticamente al elemento de envoltura. Si está convirtiendo un componente de componentes clásicos, debe asegurarse de agregar ...attributes al elemento contenedor.

Antes:

import Component from '@ember/component';

export default Component.extend(
  text: 'Hello, world!'
);
this.text

Después:

import Component from '@glimmer/component';

export default class HelloComponent extends Component 
  text = 'Hello, world!';

this.text

Argumentos

En los componentes de la clase, los argumentos se asignan directamente a la instancia de clase. Esto ha causado muchos problemas a lo largo de los años, desde la sobrescritura de métodos y acciones hasta un código poco claro donde la diferencia entre los valores de clase internos y los argumentos es difícil de razonar.

Los nuevos componentes resuelven esto colocando todos los argumentos en un objeto disponible como args propiedad.

Antes:

import Component from '@ember/component';
import  computed  from '@ember/object';

export default Component.extend(
  width: 0,
  height: 0,

  aspectRatio: computed('width', 'height', function() 
    return this.width / this.height;
  )
);
!-- Usage --

Después:

import Component from '@glimmer/component';

export default class ImageComponent extends Component 
  get aspectRatio() 
    return this.args.width / this.args.height;
  

!-- Usage --

args y sus valores se rastrean automáticamente, por lo que no es necesario anotarlos, el aspectRatio getter invalidará correctamente cuando cambien y el componente se volverá a procesar (si aspectRatio se utiliza en la plantilla).

Adicionalmente, args es no mutable y se congela en los modos de desarrollo. Esto es en parte para evitar que la gente intente realizar enlaces bidireccionales (lo que no funciona, esto se analiza con más detalle a continuación) y en parte para garantizar que args siempre permanece sincronizado con los argumentos pasados ​​al componente, por lo que puede ser la “fuente única de verdad” canónica. Si desea proporcionar valores predeterminados a un argumento, debe usar un captador.

Antes:

import Component from '@ember/component';
import  computed  from '@ember/object';

export default Component.extend(
  width: 0,
  height: 0,

  aspectRatio: computed('width', 'height', function() 
    return this.width / this.height;
  )
);

Después:

import Component from '@glimmer/component';

export default class ImageComponent extends Component 
  get width() 
    return this.args.width ?? 0;
  

  get height() 
    return this.args.height ?? 0;
  

  get aspectRatio() 
    return this.args.width / this.args.height;
  

Flujo de datos unidireccional

Los argumentos del componente clásico son enlace bidireccional. Esto significa que cuando colocar un valor en el componente, también cambia el valor en el padre componente:

// components/parent.js
import Component from '@ember/component';

export default Component.extend(
  value: 'Hello, world!'
);
!-- templates/components/parent.hbs --

// components/child.js
import Component from '@ember/component';

export default Component.extend(
  click() 
    this.set('value', 'Hello, moon!');
  
);
!-- templates/components/child.hbs --

En esta configuración, cuando hacemos clic en el botón del componente secundario, actualizará el valor tanto en el componente secundario y el componente principal. Esta característica llevó a muchos patrones de datos problemáticos en componentes clásicos, donde las mutaciones ocurrirían aparentemente al azar. Era difícil averiguar qué estaba provocando los cambios y depurarlos.

Para los componentes de Glimmer, los argumentos son límite unidireccional. No hay forma de mutar directamente un valor en un componente principal del componente secundario, incluso si se pasa como argumento. En su lugar, debe enviar un acción hacia arriba para mutar el valor:

// components/parent.js
import Component from '@glimmer/component';

export default class ParentComponent extends Component 
  value = 'Hello, world!';

  @action
  updateValue(newValue) 
    this.value = newValue;
  

!-- templates/components/parent.hbs --

// components/child.js
import Component from '@ember/component';

export default class ChildComponent extends Component 
!-- templates/components/child.hbs --

En nuestra nueva configuración, el componente principal tiene una acción que establece el nuevo valor. Pasamos esta acción al componente hijo, y el componente hijo lo asigna directamente al hacer clic en el botón, utilizando el on modificador. También pasa el valor que queremos llamar al @onClick utilizando el fn ayudante. No necesitamos ninguna lógica adicional en la clase secundaria en sí; de hecho, esto podría convertirse en un componente solo de plantilla en este punto.

Este patrón se conoce como Datos abajo, acciones arriba, o flujo de datos unidireccional. Para estos nuevos componentes, este patrón se aplica: todas las mutaciones deben ocurrir a través de acciones. Esto aclara el flujo de datos, porque es posible ver de inmediato dónde están ocurriendo todas las mutaciones.

Modificadores y ganchos de ciclo de vida

Como mencionamos anteriormente, los componentes solo tienen dos ganchos de ciclo de vida, constructor y willDestroy. Había una serie de otros ganchos del ciclo de vida que existían en los componentes clásicos que generalmente estaban relacionados con la actualización del estado del componente o la manipulación del DOM:

  • willInsertElement
  • didInsertElement
  • willDestroyElement
  • didDestroyElement
  • willRender
  • didRender
  • willUpdate
  • didUpdate
  • didReceiveAttrs
  • didUpdateAttrs

Por lo general, estos pueden reemplazarse mediante el uso de captadores, en los casos en los que estén relacionados con la actualización del estado del componente, o mediante el uso de modificadores. Por ejemplo, instalar el ember-render-modifiers El complemento te dará la posibilidad de usar did-insert y did-update. ¡También puedes escribir tus propios modificadores! Siga leyendo a continuación para obtener más información.

Actualizando el estado del componente

Si anteriormente hizo algo como esto en su didReceiveAttrs o didUpdateAttrs manos:

import Component from '@ember/component';

export default Component.extend(
  didUpdateAttrs() 
    this._super(...arguments);

    if (this.disabled) 
      // clear input value
      this.set('value', '');
    
  ,

  @action
  updateValue(newValue) 
    this.set('value', newValue);

    if (this.onChange) 
      this.onChange(newValue);
    
  
);

En su lugar, puede modelar esto a través de getters y setters, derivando el valor del estado de su componente:

import Component from '@glimmer/component';
import  tracked  from '@glimmer/tracking';

export default class TextComponent extends Component 
  @tracked _value;

  get value() 
    if (this.args.disabled) 
      return (this._value = '');
    

    return this._value;
  

  @action
  updateValue(newValue) 
    this._value = newValue;

    if (this.args.onChange) 
      this.args.onChange(newValue);
    
  

Notarás que este captador es mutante el valor cuando el componente Texto está deshabilitado. Si esto le parece un olor a código, probablemente lo sea, y es una señal de que estamos administrando el estado en el nivel incorrecto. En este caso, por ejemplo, deberíamos considerar convertir el componente de texto en un componente sin estado y mutar el valor en el mismo lugar donde el disabled está establecido: El componente principal.

// components/form.js
import Component from '@glimmer/component';
import  tracked  from '@glimmer/tracking';

export default class FormComponent extends Component 
  @tracked text;
  @tracked disabled;

  @action
  updateText(text) 
    this.text = text;
  

  @action
  updateDisabled(disabled) 
    this.disabled = disabled;

    if (disabled) 
      this.text = '';
    
  

!-- templates/components/form.hbs --


import Component from '@glimmer/component';

export default class TextComponent extends Component 
  @action
  updateValue(newValue) 
    if (this.args.onChange) 
      this.args.onChange(newValue);
    
  

Ahora el componente de texto no tiene ningún estado interno, difiere del componente de formulario principal, y cuando el componente de formulario cambia su estado deshabilitado, borra el estado del texto. La mutación de estado se centraliza en la acción donde ocurre, lo que hace que nuestro programa sea más fácil de razonar en su conjunto.

Manipulación DOM

En los casos en que estaba usando los ganchos para manipular el DOM, puede actualizar para usar modificadores. Por ejemplo, supongamos que está agregando un detector de eventos al element en tu componente didInsertElement gancho y sacándolo en willDestroyElement:

import Component from '@ember/component';

export default Component.extend(
  didInsertElement() 
    this._super(...arguments);

    this.listener = e => 
      this.set('scrollOffset', e.clientY);
    ;

    this.element.addEventListener(`scroll`, this.listener);
  ,

  willDestroyElement() 
    this.element.removeEventListener(`scroll`, this.listener);

    this._super(...arguments);
  
);

Esto podría reescribirse usando el did-insert y will-destroy modificadores, si instala modificadores de renderizado de brasas en tu aplicación:

import Component from '@glimmer/component';
import  tracked  from '@glimmer/tracking';
import  action  from '@ember/object';

export default class ScrollComponent extends Component 
  @tracked scrollOffset;

  @action
  listener(e) 
    this.scrollOffset = e.clientY;
  

  @action
  registerListener(element) 
    element.addEventListener('scroll', this.listener);
  

  @action
  unregisterListener(element) 
    element.removeEventListener('scroll', this.listener);
  

...

Estos modificadores ejecutan la función que se les pasa cuando el elemento se aplican a se insertan o eliminan del DOM. Esto hace que los ganchos sean explícitos en el elemento sobre el que actúan. También hay una did-update modificador, que no se ejecuta en la inserción, pero se ejecuta siempre que cualquiera de sus valores pasados cambio, lo que le permite actualizar el elemento:

import Component from '@glimmer/component';
import  tracked  from '@glimmer/tracking';
import  action  from '@ember/object';

export default class ScrollComponent extends Component 
  @action
  setColor(element, color) 
    element.style.color = color;
  

...

Estos tres modificadores son modificadores básicos que le permiten cubrir la mayor parte de la funcionalidad que contienen los ganchos del ciclo de vida.

Escribiendo tu modificadores propios

También hay API de la comunidad disponibles para escribir sus propios modificadores, como modificador de ascuas. Ember en sí tiene API de bajo nivel conocidas como administradores de modificadores que se puede utilizar para escribir estas API de nivel superior. En general, se recomienda utilizar un complemento de la comunidad para escribir modificadores y no para escribir su propio administrador de modificadores.

Veamos cómo se vería nuestro primer ejemplo si lo escribiéramos como un modificador usando ember-modifier:

import  modifier  from 'ember-modifier';

export default modifier((element, [eventName, listener]) => 
  element.addEventListener(eventName, listener);

  return () => element.removeEventListener(eventName, listener);
);
import Component from '@glimmer/component';
import  tracked  from '@glimmer/tracking';
import  action  from '@ember/object';

export default class ScrollComponent extends Component 
  @tracked scrollOffset;

  @action
  listener(e) 
    this.scrollOffset = e.clientY;
  

...

Este modificador generaliza la funcionalidad que el componente implementó usando ganchos de ciclo de vida antes, por lo que podemos usar este modificador siempre que lo necesitemos en alguna componente. ¡Esta es una solución mucho mejor que administrar manualmente los oyentes de eventos cada vez que lo necesitamos! En este punto, el modificador es efectivamente el mismo que el on modificador también, por lo que podríamos deshacernos de él por completo y reemplazarlo con on:

...

Componentes solo de plantilla

En Octane, los componentes de plantilla solo tienen un hbs archivo y no JavaScript expediente. Detrás de escena, los componentes solo de plantilla heredan de '@glimmer/component'.

Pueden considerarse como funcional componentes, en el sentido de que su salida (la plantilla renderizada) es una función pura de sus entradas (sus argumentos). El hecho de que no puedan tener un estado los hace mucho más fáciles de razonar en general y menos propensos a errores.

Los componentes de solo plantilla no tienen una instancia de clase de respaldo, por lo que this en sus plantillas es null. Esto significa que solo puede hacer referencia a argumentos pasados ​​a través de la sintaxis de argumento con nombre (por ejemplo, @arg):

!--
  This does not work, since `this` does not exist
--


Además, el mut helper generalmente no se puede usar por la misma razón:

!-- This does not work --