340 lines
8.4 KiB
JavaScript
340 lines
8.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.component('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();
|
|
}
|
|
|
|
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;
|
|
this.emit('dataChange');
|
|
this.emit('dataUpdate');
|
|
}
|
|
|
|
/**
|
|
* Removes a row from the model and emits the 'rowRemove' event.
|
|
*
|
|
* @param {Number} index The row index
|
|
*/
|
|
remove(index) {
|
|
let [item] = this.data.splice(index, 1);
|
|
|
|
let proxiedIndex = this.proxiedData.indexOf(item);
|
|
this.proxiedData.splice(proxiedIndex, 1);
|
|
|
|
if (!item.$isNew)
|
|
this.removed.push(item);
|
|
|
|
this.isChanged = true;
|
|
this.emit('rowRemove', index);
|
|
this.emit('dataUpdate');
|
|
if (this.autoSave)
|
|
this.save();
|
|
}
|
|
|
|
/**
|
|
* 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.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.component('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 {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.
|
|
*
|
|
* @return {Promise} The request promise
|
|
*/
|
|
loadMore() {
|
|
return Promise.resolve();
|
|
}
|
|
}
|