Luego de de esta larga selección de datos pudimos solucionar este apuro que presentan muchos de nuestros lectores. Te dejamos la respuesta y nuestro objetivo es resultarte de mucha apoyo.
¡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 necesitanEmberObject
API, comoreopenClass
,extend
. Puede utilizar de forma seguraconstructor
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 --
Comentarios y valoraciones
Recuerda que tienes permiso de agregar una reseña si te fue de ayuda.