ember-changeset
Version:
Changesets for Ember
263 lines (215 loc) • 6.54 kB
JavaScript
import { assert } from '@ember/debug';
import { dependentKeyCompat } from '@ember/object/compat';
import { BufferedChangeset } from 'validated-changeset';
import { Changeset as ValidatedChangeset } from './validated-changeset';
import ArrayProxy from '@ember/array/proxy';
import ObjectProxy from '@ember/object/proxy';
import { notifyPropertyChange } from '@ember/object';
import mergeDeep from './utils/merge-deep';
import isObject from './utils/is-object';
import { tracked } from '@glimmer/tracking';
import { get as safeGet, set as safeSet } from '@ember/object';
import { macroCondition, dependencySatisfies, importSync } from '@embroider/macros';
export { ValidatedChangeset };
const CHANGES = '_changes';
const PREVIOUS_CONTENT = '_previousContent';
const CONTENT = '_content';
const defaultValidatorFn = () => true;
export function buildOldValues(content, changes, getDeep) {
const obj = Object.create(null);
for (let change of changes) {
obj[change.key] = getDeep(content, change.key);
}
return obj;
}
function isProxy(o) {
return !!(o && (o instanceof ObjectProxy || o instanceof ArrayProxy));
}
function maybeUnwrapProxy(o) {
return isProxy(o) ? maybeUnwrapProxy(safeGet(o, 'content')) : o;
}
let Model;
if (macroCondition(dependencySatisfies('ember-data', '*'))) {
Model = importSync('@ember-data/model').default;
}
export class EmberChangeset extends BufferedChangeset {
_changes;
_errors;
_content;
isObject = isObject;
maybeUnwrapProxy = maybeUnwrapProxy;
// DO NOT override setDeep. Ember.set does not work wth empty hash and nested
// key Ember.set({}, 'user.name', 'foo');
// override base class
// DO NOT override setDeep. Ember.set does not work with Ember.set({}, 'user.name', 'foo');
getDeep = safeGet;
mergeDeep = mergeDeep;
safeGet(obj, key) {
if (Model && obj.relationshipFor?.(key)?.meta?.kind == 'belongsTo') {
return obj.belongsTo(key).value();
}
return safeGet(obj, key);
}
safeSet(obj, key, value) {
return safeSet(obj, key, value);
}
/**
* @property isValid
* @type {Array}
*/
get isValid() {
return super.isValid;
}
/**
* @property isInvalid
* @type {Boolean}
*/
get isInvalid() {
return super.isInvalid;
}
/**
* @property isPristine
* @type {Boolean}
*/
get isPristine() {
return super.isPristine;
}
/**
* @property isDirty
* @type {Boolean}
*/
get isDirty() {
return super.isDirty;
}
get pendingData() {
let content = this[CONTENT];
let changes = this[CHANGES];
let pendingChanges = this.mergeDeep(Object.create(Object.getPrototypeOf(content)), content, { safeGet, safeSet });
return this.mergeDeep(pendingChanges, changes, { safeGet, safeSet });
}
/**
* Manually add an error to the changeset. If there is an existing
* error or change for `key`, it will be overwritten.
*
* @method addError
*/
addError(key, error) {
super.addError(key, error);
notifyPropertyChange(this, key);
// Return passed-in `error`.
return error;
}
/**
* Manually push multiple errors to the changeset as an array.
*
* @method pushErrors
*/
pushErrors(key, ...newErrors) {
const { value, validation } = super.pushErrors(key, ...newErrors);
notifyPropertyChange(this, key);
return { value, validation };
}
/**
* Sets property or error on the changeset.
* Returns value or error
*/
_setProperty({ key, value, oldValue }) {
super._setProperty({ key, value, oldValue });
notifyPropertyChange(this, key);
}
/**
* Notifies virtual properties set on the changeset of a change.
* You can specify which keys are notified by passing in an array.
*
* @private
* @param {Array} keys
* @return {Void}
*/
_notifyVirtualProperties(keys) {
keys = super._notifyVirtualProperties(keys);
(keys || []).forEach((key) => notifyPropertyChange(this, key));
return;
}
/**
* Deletes a key off an object and notifies observers.
*/
_deleteKey(objName, key = '') {
const result = super._deleteKey(objName, key);
notifyPropertyChange(this, key);
return result;
}
/**
* Executes the changeset if in a valid state.
*
* @method execute
*/
execute() {
let oldContent;
if (this.isValid && this.isDirty) {
let content = this[CONTENT];
let changes = this[CHANGES];
// keep old values in case of error and we want to rollback
oldContent = buildOldValues(content, this.changes, this.getDeep);
// we want mutation on original object
// @tracked
this[CONTENT] = this.mergeDeep(content, changes, { safeGet, safeSet });
}
this[PREVIOUS_CONTENT] = oldContent;
return this;
}
}
/**
* Creates new changesets.
*/
export function changeset(obj, validateFn = defaultValidatorFn, validationMap = {}, options = {}) {
assert('Underlying object for changeset is missing', Boolean(obj));
assert('Array is not a valid type to pass as the first argument to `changeset`', !Array.isArray(obj));
if (options.changeset) {
return new options.changeset(obj, validateFn, validationMap, options);
}
const c = new EmberChangeset(obj, validateFn, validationMap, options);
return c;
}
/**
* Creates new changesets.
* @function Changeset
*/
export function Changeset(obj, validateFn = defaultValidatorFn, validationMap = {}, options = {}) {
const c = changeset(obj, validateFn, validationMap, options);
return new Proxy(c, {
get(targetBuffer, key /*, receiver*/) {
const res = targetBuffer.get(key.toString());
return res;
},
set(targetBuffer, key, value /*, receiver*/) {
targetBuffer.set(key.toString(), value);
return true;
},
});
}
export default class ChangesetKlass {
/**
* Changeset factory
* TODO: deprecate in favor of factory function
*
* @class ChangesetKlass
* @constructor
*/
constructor(obj, validateFn = defaultValidatorFn, validationMap = {}, options = {}) {
const c = changeset(obj, validateFn, validationMap, options);
return new Proxy(c, {
get(targetBuffer, key /*, receiver*/) {
const res = targetBuffer.get(key.toString());
return res;
},
set(targetBuffer, key, value /*, receiver*/) {
targetBuffer.set(key.toString(), value);
return true;
},
});
}
}