ui5

Cómo clonar y extender objetos en UI5

8 enero 2023 · Última revisión 12 noviembre 2022 · 4 minutos de lectura

En general, es buena práctica no modificar los datos directamente. Bajo mi punto de vista, el más importante es justamente cuando modificamos datos previamente a guardarse en un servicio. Dado el siguiente ciclo de vida de datos:

  1. Hacemos una petición GET al servidor para recuperar los datos.
  2. La aplicación modifica los datos en local previamente a la acción de guardar.
  3. Hacemos una petición POST o PUT al servidor en la cual enviamos los datos modificados.

Hasta aquí todo parece bien, pero estos simples pasos pueden tener un problema. Si el punto 3 falla, los datos que hayamos modificado en el punto 2 no estarán sincronizados con los datos del servidor.

Veamos un ejemplo sencillo:

// Partiremos de un modelo oData que en sus métodos devuelve una Promesa
const oDataModel = new ODataModelPromise();

// Al entrar en la vista, refrescamos la lista de items
async onRouteMatched() {
  const aItems = await oDataModel.read("/ItemsSet");

  this.oModel.setData(aItems);
}

// Al pulsar el botón guardar en un item,
// guardaremos los datos con la fecha de modificación actualizada
pressSaveItem(oItem) {
  oItem.updateDate = new Date();

  const sItemKey = oDataModel.createKey("/ItemsSet", oItem);
  await oDataModel.update(sItemKey, oItem);
}

Aunque este ejemplo es bastante tonto, si la modificación falla, tendremos una propiedad local modificada. Si además, esta propiedad está visible/bindada en la vista, es probable que se llegue a mostrar en algún momento. Pues bien, ese dato es erróneo. La solución es clonar el objeto antes de modificarlo:

pressSaveItem(oItem) {
  // Este objeto lo clonamos mediante la destructuración
  const oClonedItem = {...oItem};
  oClonedItem.updateDate = new Date();

  const sItemKey = oDataModel.createKey("/ItemsSet", oClonedItem);
  await oDataModel.update(sItemKey, oClonedItem);
}

Esta solución sirve únicamente si se trata de objetos planos. En UI5 es muy habitual trabajar con objetos que tienen profundidad, ya que con OData podemos hacer expand. Pues la destructuración en estos casos no funcionará como esperamos.

const oObject = { prop: { nombre: 'Artículo' } };

// Clonamos el objeto anterior
const oClone = { ...oObject };

// Modificamos la propiedad nombre de prop
oClone.prop.nombre = 'Modificado';

console.log(oObject.prop.nombre); // "Modificado"

clone, deepClone

Por este motivo, para clonar objetos y arrays es mejor utilizar clone y deepClone. Con clone podemos hacer lo mismo que acabamos de ver y con deepClone una copia con la profundidad (tiene un segundo parámetro para indicar la profundidad máxima, que por defecto es 10):

sap.ui.define([
    'sap/base/util/clone',
    'sap/base/util/deepClone'
  ], function (clone, deepClone) {

  modificarItem() {
    const oObject = { prop: { nombre: 'Artículo' } };

    const oClone = clone(oObject);
    oClone.prop.nombre = 'Modificado';

    console.log(oObject.prop.nombre); // "Modificado"
    console.log(oClone.prop.nombre); // "Modificado"

    // Asumimos que oObject vuelve al estado inicial

    const oCloneDeep = deepClone(oObject);
    oCloneDeep.prop.nombre = 'Modificado';

    console.log(oObject.prop.nombre); // "Artículo"
    console.log(oCloneDeep.prop.nombre); // "Modificado"
  }
});

extend, deepExtend, merge

También tenemos los métodos extend, deepExtend y merge. Igual que en el caso anterior, extend extenderá el primer nivel del objeto en uno nuevo, mientras que deepExtend y merge lo hará a todos los niveles. Esto puede ser muy útil para crear objetos nuevos basados en otros, a la vez que estableces valores por defecto para propiedades (en el caso de que no existan) o bien machacas las existentes en el objeto por unas nuevas:

sap.ui.define([
    'sap/base/util/extend',
    'sap/base/util/deepExtend',
    'sap/base/util/merge'
  ], function (extend, deepExtend, merge) {

  modificarItem() {
    const oObject = { prop: { nombre: 'Artículo' } };
    const oResult = { prop: { nombre: "Default", apellido: "Default" } };
    const oItem = { prop: { edad: 22 } };

    // ❌ ¡Ojo! El primer parámetro va a almacenar el resultado final
    // Si se ejecuta todo del tirón, el valor de oResult irá cambiando
    // Asumiremos que en cada linia, los objetos vuelven a su estado inicial

    extend(oResult, oObject); // { nombre: "Artículo" }
    extend(oResult, oObject, oItem); // { edad: 22 }

    deepExtend(oResult, oObject); // { nombre: "Artículo", apellido: "Default" }
    merge(oResult, oObject); // { nombre: "Artículo", apellido: "Default" }

    deepExtend(oResult, oObject, oItem); // { nombre: "Artículo", apellido: "Default", edad: 22 }
    merge(oResult, oObject, oItem); // { nombre: "Artículo", apellido: "Default", edad: 22 }


    // Tambien podemos clonar
    const oCloned = deepExtend({}, oResult, oObject, oItem); // { nombre: "Artículo", apellido: "Default", edad: 22 }
  }
});

jQuery.sap.extend

No debe utilizarse jQuery.sap.extend porque está obsoleto

Anteriormente en UI5 teníamos la función jQuery.sap.extend. Esto ya está deprecated y debe ser sustituido por algo nativo de la plataforma o los métodos mencionados en este artículo.