Auto Save en Ember Octane 💾
Tags: javascript, ember, frontend
Published: 2020-04-25 22:12:12

---
title: Auto Save en Ember Octane 💾
published: true
description: 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.
tags: 
 - JavaScript
 - Ember.js
 - Frontend
---

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 JavaScript e idealmente de Ember.js.

###### 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_](https://emberjs.com), 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](https://twitter.com/betocantu93)

##### ¿Qué es Ember.js?

Bueno primero no me gustaría suponer que conoces Ember.js, [_Ember.js_](https://emberjs.com) 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

2. 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.
3. 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.

  * ¿Quieres utilizar graphql y apollo? [ember-apollo-client](https://github.com/ember-graphql/ember-apollo-client)
  * ¿Quieres animaciones? [ember-animated](https://ember-animation.github.io/ember-animated/)
  * ¿Quieres material design? [ember-paper](https://miguelcobain.github.io/ember-paper/)
  * ¿Quieres soportar muchos lenguajes? [ember-intl](https://github.com/ember-intl/ember-intl)
  * ¿Quieres mapas? [ember-mapbox-gl](https://kturney.github.io/ember-mapbox-gl/docs)

4.  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.
5.  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`

```javascript
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

```handlebars
{{! 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`



```handlebars
{{! 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`



```handlebars
{{! 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?

```handlebars
{{! 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. 



```handlebars
{{! 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.

```javascript
// 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



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



Finalmente, podemos utilizarlo con facilidad.

```handlebars
<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`



```javascript
// 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={{fn @autoSave false}}`



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



```javascript
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` 



```javascript
// 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;
  }
}
```



```handlebars
{{! 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.