salix/front/core/components/model-proxy/model-proxy.js

384 lines
9.4 KiB
JavaScript

import ngModule from '../../module';
import Component from '../../lib/component';
/**
* A data model. It allows to filter, sort and paginate data.
*
* @property {Boolean} autoLoad Whether to load data automatically when required attributes are setted
* @event dataChange Emitted when data property changes
*/
export class DataModel extends Component {
/**
* @type {Array<Object>} A JavaScript array with the model data.
*/
get data() {
return null;
}
/**
* Refresh the model data.
*
* @return {Promise} The request promise
*/
refresh() {
return Promise.resolve();
}
/**
* Clears the model, removing all it's data.
*/
clear() {}
}
ngModule.vnComponent('vnDataModel', {
controller: DataModel,
bindings: {
data: '=?',
autoLoad: '<?',
order: '@?',
limit: '<?'
}
});
/**
* A data model that monitorizes changes to the data.
* It internally uses the JavaScript Proxy to track changes
* made in model rows.
*
* @property {Boolean} autoSave Whether to save data automatically when model is modified
* @event dataChange Emitted when data property changes
* @event dataUpdate Emitted when data property changes
* @event rowInsert Emitted when new row is inserted
* @event rowRemove Emitted when row is removed
* @event rowChange Emitted when row property is changed
* @event save Emitted when data is saved
*/
export default class ModelProxy extends DataModel {
constructor($element, $scope) {
super($element, $scope);
this.resetChanges();
this.status = 'clear';
}
get orgData() {
return this._orgData;
}
set orgData(value) {
this._orgData = value;
if (value) {
this.proxiedData = new Array(value.length);
for (let i = 0; i < value.length; i++)
this.proxiedData[i] = this.createRow(Object.assign({}, value[i]), i, value[i]);
} else
this.proxiedData = null;
this.data = null;
this.resetChanges();
}
/**
* @type {Array<Object>} A JavaScript array with the model data.
* Rows data can be modified directly but for insertion or removing
* rows use the remove() or insert() model methods, otherwise changes
* are not detected by the model.
*/
get data() {
return this._data;
}
set data(value) {
this._data = value;
if (value == null)
this.status = 'clear';
else if (value.length)
this.status = 'ready';
else
this.status = 'empty';
this.emit('dataChange');
this.emit('dataUpdate');
}
/**
* Removes a row from the model and emits the 'rowRemove' event.
*
* @param {Number} index The row index
* @return {Promise} The save request promise
*/
remove(index) {
let [row] = this.data.splice(index, 1);
let proxiedIndex = this.proxiedData.indexOf(row);
this.proxiedData.splice(proxiedIndex, 1);
if (!row.$isNew)
this.removed.push(row);
this.isChanged = true;
if (!this.data.length)
this.status = 'empty';
this.emit('rowRemove', index);
this.emit('dataUpdate');
if (this.autoSave)
return this.save();
else
return this.$q.resolve();
}
/**
* Removes a row from the model and emits the 'rowRemove' event.
*
* @param {Object} row The row object
* @return {Promise} The save request promise
*/
removeRow(row) {
return this.remove(this.data.indexOf(row));
}
/**
* Inserts a new row into the model and emits the 'rowInsert' event.
*
* @param {Object} data The initial data for the new row
* @return {Number} The inserted row index
*/
insert(data) {
data = Object.assign(data || {}, this.link);
let newRow = this.createRow(data, null);
newRow.$isNew = true;
let index = this.proxiedData.push(newRow) - 1;
if (this.data)
this.data.push(newRow);
this.isChanged = true;
this.status = 'ready';
this.emit('rowInsert', index);
this.emit('dataUpdate');
return index;
}
createRow(obj, index, orgRow) {
Object.assign(obj, {
$orgIndex: index,
$orgRow: orgRow,
$oldData: null,
$isNew: false
});
return new Proxy(obj, {
set: (obj, prop, value) => {
let changed = prop.charAt(0) !== '$' && value !== obj[prop];
if (changed && !obj.$isNew) {
if (!obj.$oldData)
obj.$oldData = {};
if (!obj.$oldData[prop])
obj.$oldData[prop] = value;
this.isChanged = true;
}
let ret = Reflect.set(obj, prop, value);
if (changed) {
this.emit('rowChange', {obj, prop, value});
this.emit('dataUpdate');
if (!obj.$isNew && this.autoSave)
this.save();
}
return ret;
}
});
}
resetChanges() {
this.removed = [];
this.isChanged = false;
let data = this.proxiedData;
if (data) {
for (let row of data)
row.$oldData = null;
}
}
/**
* Applies all changes made to the model into the original data source.
*/
applyChanges() {
let data = this.proxiedData;
let orgData = this.orgData;
if (!data) return;
for (let row of data) {
if (row.$isNew) {
let orgRow = {};
for (let prop in row) {
if (prop.charAt(0) !== '$')
orgRow[prop] = row[prop];
}
row.$orgIndex = orgData.push(orgRow) - 1;
row.$orgRow = orgRow;
row.$isNew = false;
} else if (row.$oldData) {
for (let prop in row.$oldData)
row.$orgRow[prop] = row[prop];
}
}
let removed = this.removed;
if (removed) {
removed = removed.sort((a, b) => b.$orgIndex - a.$orgIndex);
for (let row of this.removed)
orgData.splice(row.$orgIndex, 1);
}
this.resetChanges();
}
/**
* Should be implemented by child classes.
*/
save() {
this.emit('save');
}
/**
* Undoes all changes made to the model data.
*/
undoChanges() {
let data = this.proxiedData;
if (!data) return;
for (let i = 0; i < data.length; i++) {
let row = data[i];
if (row.$oldData)
Object.assign(row, row.$oldData);
if (row.$isNew)
data.splice(i--, 1);
}
let removed = this.removed;
if (removed) {
removed = removed.sort((a, b) => a.$orgIndex - b.$orgIndex);
for (let row of this.removed)
data.splice(row.$orgIndex, 0, row);
}
this.resetChanges();
}
}
ngModule.vnComponent('vnModelProxy', {
controller: ModelProxy,
bindings: {
orgData: '<?',
data: '=?'
}
});
/**
* Interface used to filter data coming from different datasources.
*
* @property {Object} filter The base filter
* @property {Object} params The base filter params
*/
export class Filtrable {
/**
* Applies a filter to the model clearing the previous ones.
*
* @param {Object} filter The filter parameters
* @param {*} params Custom user parameters
* @return {Promise} The request promise
*/
applyFilter() {
return Promise.resolve();
}
/**
* Adds a filter to the model mixing it with the currently applied filters.
*
* @param {Object} filter The filter parameters
* @param {*} params Custom user parameters
* @return {Promise} The request promise
*/
addFilter() {
return Promise.resolve();
}
/**
* Removes the currently applied filters.
*
* @return {Promise} The request promise
*/
removeFilter() {
return Promise.resolve();
}
}
/**
* Interface used to sort data coming from different datasources using
* the same specification: columnName [ASC|DESC]
*
* @property {String|Array<String>|Function} order The sort specification.
*/
export class Sortable {}
/**
* Interface used to paginate data coming from different datasources.
*
* @property {Number} limit The page size
*/
export class Paginable {
/**
* @type {Boolean} Whether the model is loading.
*/
get isLoading() {
return false;
}
/**
* @type {ready|loading|clear|empty|error} The current model status.
*/
get status() {
return null;
}
/**
* @type {Boolean} Whether the model is paging.
*/
get isPaging() {
return false;
}
/**
* @type {Boolean} Whether the data is paginated and there are more rows to load.
*/
get moreRows() {
return false;
}
/**
* When limit is enabled, loads the next set of rows.
*
* @param {Boolean} append - Whether should append new data
* @return {Promise} The request promise
*/
loadMore(append) {
return Promise.resolve(append);
}
}