Este es un blog-post para desarrolladores de JavaScript y sobre todo de Ember.js, es avanzado por lo cual supone que tienes conocimiento de ambas.

Una nota antes de empezar

Este ser谩 el primer blog que hago en espa帽ol, considero que falta mucho contenido sobre tecnolog铆a en espa帽ol y espec铆ficamente de Ember.js moderno, el cual forma parte de mi d铆a a d铆a como desarrollador. Para no forzar traducciones cuando complican el texto, voy a dejar un glosario al final cuando considere necesario, sin embargo si tienes alguna duda siempre puedes localizarme en twitter @betocantu93

驴Qu茅 es Ember.js?

Bueno primero no me gustar铆a suponer que conoces Ember.js, Ember.js es un framework de JavaScript para construir aplicaciones web robustas y escalables, sus principales atractivos son:

  1. Bater铆as inclu铆das, un proyecto nuevo de Ember.js incluye entre muchas otras cosas:
  • Build pipeline (olv铆date de estar configurando Rollup, Webpack o dem谩s)
  • Un router
  • Data Layer (Ember Data, para manejar los datos en tu proyecto)
  • Testing
  • Performance
  1. Las convenciones, permiten desarrollar un proyecto web escalable de manera ordenada y predecible, un desarrollador de Ember.js puede brincar de proyecto en proyecto sin ning煤n problema ya que se sentir谩 en casa con c贸digo muy familiar.
  2. Addons, son como "plugins" que le puedes a帽adir a tu aplicaci贸n, paquetes de npm que gracias a las convenciones de Ember.js, pueden a帽adir c贸digo que aumente la funcionalidad de tu aplicaci贸n de manera asertiva, incremental y poderosa.
  1. C贸digo Libre, Ember.js es desarrollado por la comunidad alrededor del 馃實, todos pueden contribu铆r y no hay ninguna empresa entorpeciendo con sus intereses.
  2. Fuerte versionamiento, Ember.js tiene un compromiso con no romper las aplicaciones de todos entre versiones ya que tiene un fuerte sistema de versionamiento y de ir removiendo c贸digo obsoleto, puedes estar seguro de que tu aplicaci贸n podr谩 migrar a nuevas versiones sin tener que reescribirla a atrav茅s de los a帽os.

Auto Save 馃捑

Pero, 驴qu茅 es Auto Save?

Auto save es guardar un modelo o documento cada vez que ciertos eventos sucedan, esto tiene dos beneficios importantes

  1. Mejora la experiencia de usuario al no tener que estar dando clic en 馃捑 a cada cambio en documentos o formularios grandes
  2. Puede evitar el miedo de perder la informaci贸n

Basta de intros, empecemos a ver c贸digo.

Nuestro modelo Person

import Model, { attr } from '@ember-data/model';

export default class PersonModel extends Model {
  @attr firstName;
  @attr lastName;
  @attr birthday;
}

Primero que nada necesitamos un componente para nuestro formulario b谩sico tradicional y lo iremos poco a poco mejorando, en este caso estar茅 utilizando ember-paper por facilidad

{{! components/edit-person/index.hbs }}
<PaperForm @onSubmit={{@onSubmit}} as |Form|>
    <Form.input @onChange={{fn (mut @person.firstName)}} @value={{@person.firstName}} />
    <Form.input @onChange={{fn (mut @person.lastName)}} @value={{@person.lastName}} />
    <Form.input @onChange={{fn (mut @person.birthday)}} @value={{@person.birthday}} />

    <Form.on-submit>
      Guardar
     </Form.on-submit>
</PaperForm>

Bueno y 驴c贸mo lo uso? Muy f谩cil, hay que suponer que el this.save es una funci贸n en el controller que se encarga de hacer this.model.save

{{! templates/person.hbs}}
<EditPerson 
    @person={{this.model}} 
    @onSubmit={{this.save}}
/>

Ahora la primer cosa que puede mejorar un poco para hacer m谩s f谩cil el trabajo con estas tribialidades como crear un controller para definir una acci贸n this.save con this.model.save, utilizando ember-composable-helpers puedes simplemente utilizar el helper de invoke

{{! templates/person.hbs}}
<EditPerson 
    @person={{this.model}} 
    @onSubmit={{invoke "save" this.model}}
/>

Ahora, 驴c贸mo se ver铆a nuestro componente de EditPerson si reemplazamos el bot贸n de guardar por una funci贸n de autosave?

{{! components/edit-person/index.hbs }}
<PaperForm as |Form|>
    <Form.input 
        @onChange={{fn (mut @person.firstName)}} 
        @value={{@person.firstName}}
        @onBlur={{@autoSave}}
     />
    <Form.input 
        @onChange={{fn (mut @person.lastName)}} 
        @value={{@person.lastName}} 
        @onBlur={{@autoSave}}
  />
    <Form.input 
        @onChange={{fn (mut @person.birthday)}} 
        @value={{@person.birthday}}
        @onBlur={{@autoSave}}
   />
</PaperForm>

De esta manera, cuando el usuario salga del input (onBlur) se estar谩 ejecutando una funci贸n de autoSave, esto deja al que invoca el componente la decisi贸n de c贸mo hacer el autosave.

{{! templates/person.hbs}}
<EditPerson @person={{this.model}} @autoSave={{invoke "save" this.model}}/>

Esto funciona, pero qu茅 tal si el usuario utilizara [ Tab ] para moverse por tu forma, estar铆as haciendo muchos {{invoke "save" this.model}} probablemente innecesarios, para eso vamos a introducir un nuevo concepto

El Componente AutoSave

Este componente nos ayudar谩 a encapsular l贸gica de auto save y que vamos a poder "inyectarla" en cualquier template.

// components/auto-save/index.js
import GlimmerComponent from '@glimmer/component';
import { task, timeout } from 'ember-concurrency';

export default class AutoSaveComponent extends GlimmerComponent {  
  @(task(function(){
    yield timeout(500);
    try {
      return yield this.args.model.save();
    } finally {
      //Si hay un error, de permisos, por ejemplo.
      this.args.model.rollbackAttributes();
      //manera f谩cil de no tener que guardar track de el dirtinessde las relaciones
      this.args.model.reload();
    }
  }).keepLatest()) autoSaveTask;
}

Para evitar que nuestra funci贸n se dispare de maneras no controladas, podemos utilizar el addon ember-concurrency que de una manera muy declarativa nos deja implementar el patr贸n de debouncing, que obliga a que una funci贸n no sea llamada otra vez hasta que cierto tiempo haya pasado sin ser llamada otra vez, algo as铆 como "Ejecuta esta funci贸n, solo si 500 milisegundos han pasado sin que se haya vuelto a llamar".

驴Pero c贸mo utilizar este componente? Necesitamos primero hacer yield de nuestra task en nuestro template

{{! components/auto-save/index.hbs }}
{{yield (perform this.autoSaveTask)}}

Finalmente, podemos utilizarlo con facilidad.

<AutoSave @model={{this.model}} as |autoSave|>
    <EditPerson @person={{this.model}} @autoSave={{autoSave}} />
</AutoSave>

Me gusta este patr贸n en el cual abstraemos una funcionalidad a un componente, de esa manera podemos reutilizar c贸digo sin tener que utilizar Mixins, son como Mixins pero en "Render", interesante.

Todav铆a tenemos algunos casos extremos a resolver

  1. Con el dise帽o actual, podemos estar ejecutando guardar a煤n y cuando no es necesario, ya que puede que no haya cambiado el valor, por lo tanto innecesario.
  2. ember-concurrency cancela todas las task cuando el objeto en el que viven es destru铆do. Como estamos manejando la creaci贸n de la task en un componente, hay un caso extremo en donde el usuario podr铆a hacer cambios en el modelo y cambiarse de ruta por el alg煤n bot贸n de tu UI, mientras a煤n se est谩 ejectuando la estrategia de debounce (esperando los 500ms) en la task, por ende puede ser que no se complete y guarde nuestro modelo lo que representa posible p茅rdida de informaci贸n y es intolerable.

Para resolver el punto 1, podemos agregar un check antes del debouncing

// components/auto-save/index.js
import GlimmerComponent from '@glimmer/component';
import { task, timeout } from 'ember-concurrency';

export default class AutoSaveComponent extends GlimmerComponent {  
    /*
        @param {Boolean} checkIfDirtyAttributes Verificar si el modelo tiene cambios 
    */
  @(task(function(checkIfDirtyAttributes = true){
    if(
      checkIfDirtyAttributes && 
      this.args.model.get('hasDirtyAttributes')
    ) { 
      yield timeout(500);
      try {
        return yield this.args.model.save();
      } finally {
        //Si hay un error, de permisos, por ejemplo.
        this.args.model.rollbackAttributes();
        //manera f谩cil de no tener que guardar track de el dirtinessde las relaciones
        this.args.model.reload();
      }
    }
  }).keepLatest()) autoSaveTask;
}

Ember Data lleva un registro en todo momento si el atributo de un modelo cambi贸, pero no de si sus relaciones han cambiado, entonces si quieres guardar por que se cambi贸 una relaci贸n, puedes ignorar los atributos, @onChange={{@autoSave false}}

Para resolver el punto 2, podemos mover el task del componente al modelo en s铆.

import Model, { attr } from '@ember-data/model';
import { task, timeout } from 'ember-concurrency';

export default class PersonModel extends Model {
  @attr firstName;
  @attr lastName;
  @attr birthday;

  /*
        @param {Boolean} shouldRun puede o no ejecutarse?
    */
  @(task(function(shouldRun = function(){return true;}, checkIfDirtyAttributes = true){
    shouldRun = typeOf(shouldRun) === 'function' ? shouldRun(this) : shouldRun;

    if(!shouldRun) { return; }
    if(checkIfDirtyAttributes && !this.get('hasDirtyAttributes')) { return; }

    yield timeout(500);
    try {
      return yield this.save();
    } finally {
      //Si hay un error, de permisos, por ejemplo.
      this.rollbackAttributes();
      //manera f谩cil de no tener que guardar track de el dirtinessde las relaciones
      this.reload();
    }

  }).keepLatest()) autoSaveTask;
}

Como podemos ver, reemplazamos el this.args.model por this, porque ahora esta task vive en un modelo en s铆, adem谩s para hacerla un poco m谩s flexible y reutilizable, permitimos que nos envi茅n una funci贸n que retorne un boolean o un boolean en s铆 para saber si correr o no el task.

Tenemos que actualizar nuestro componente AutoSave

// components/auto-save/index.js
import GlimmerComponent from '@glimmer/component';
import { task, timeout } from 'ember-concurrency';

export default class AutoSaveComponent extends GlimmerComponent {  
    get shouldRun() {
    /*
        Podr铆amos tener aqu铆 cualquier validaci贸n extra, 
        por ejemplo, permisos de ember-can
    */
    return true;
  }
}
{{! components/auto-save/index.hbs }}
{{yield (perform @model.autoSaveTask this.shouldRun)}}

Al final, nuestro componente <AutoSave /> se puede decir que es un helper / mixin que funciona como middleware para ayudarnos a ejecutar la task en nuestro modelo.

Es un patr贸n algo complejo, pero permite agregar agregar f谩cil funcionalidad de autoSave a cualquier formulario de manera sencilla, ideal para aplicaciones modernas.