@mlightcad/common
Version:
[](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/@mlightcad/common)
204 lines • 7.68 kB
JavaScript
/**
* @fileoverview Object model implementation for the AutoCAD Common library.
*
* This module provides a reactive object model with attribute management,
* change tracking, and event notification. Inspired by Backbone.js Model
* but with TypeScript support and reduced dependencies.
*
* @module AcCmObject
* @version 1.0.0
*/
import { AcCmEventManager } from './AcCmEventManager';
import { clone, defaults, has, isEmpty, isEqual } from './AcCmLodashUtils';
/**
* This class is used to store attributes of one data model. It has the following benifits.
* - Get notification when value of one attributes is changed
* - Have one `changed` property to store all of changed values
* - Store all of states of one model in one key/value object and make it is easy to serialize/deserialize model and model changes
*
* Actually implementation of this class is based on class Model in Backbone.js. However, Model class in Backbone
* is too heavy. So we implement it again based on source code of class Model in Backbone.js. Morever, we want to
* keep our library with less external dependencies and don't want to introduce depenedency on Backbone.js.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var AcCmObject = /** @class */ (function () {
/**
* Create one object to store attributes. For performance reason, values of attributes passed to constructor
* will not be cloned to `attributes` property. So it means that value of `attributes` property in this object
* is just a reference to arguments `attributes` passed to constructor.
* @param attributes Input attributes to store in this object
*/
function AcCmObject(attributes, defaultAttrs) {
this.events = {
attrChanged: new AcCmEventManager(),
modelChanged: new AcCmEventManager()
};
this._changing = false;
this._previousAttributes = {};
this._pending = false;
var attrs = attributes || {};
if (defaultAttrs) {
defaults(attrs, defaultAttrs);
}
this.attributes = attrs;
this.changed = {};
}
/**
* Gets the value of an attribute.
*
* For strongly-typed access to attributes, use the `get` method privately in public getter properties.
*
* @template A - The key type extending string keys of T.
* @param {A} key - The attribute key to retrieve.
* @returns {T[A] | undefined} The attribute value or undefined if not set.
*
* @example
* ```typescript
* // Get a single attribute value
* const name = obj.get('name')
* const visible = obj.get('visible')
*
* // Check if attribute exists
* if (obj.get('name') !== undefined) {
* console.log('Name is set')
* }
*
* // For strongly-typed subclasses
* get name(): string {
* return super.get("name")
* }
* ```
*/
AcCmObject.prototype.get = function (key) {
return this.attributes[key];
};
AcCmObject.prototype.set = function (key, val, options) {
if (key == null)
return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
}
else {
attrs = {};
attrs[key] = val;
}
options || (options = {});
// Extract attributes and options.
var unset = options.unset;
var silent = options.silent;
var changes = [];
var changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = clone(this.attributes);
this.changed = {};
}
var current = this.attributes;
var changed = this.changed;
var prev = this._previousAttributes;
// For each `set` attribute, update or delete the current value.
for (var attr in attrs) {
val = attrs[attr];
if (!isEqual(current[attr], val))
changes.push(attr);
if (!isEqual(prev[attr], val)) {
changed[attr] = val;
}
else {
delete changed[attr];
}
unset ? delete current[attr] : (current[attr] = val);
}
// Trigger all relevant attribute changes.
if (!silent) {
// @ts-expect-error just keep backbone implementation as is
if (changes.length)
this._pending = options;
for (var i = 0; i < changes.length; i++) {
this.events.attrChanged.dispatch({
object: this,
attrName: changes[i],
attrValue: current[changes[i]],
options: options
});
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing)
return this;
if (!silent) {
while (this._pending) {
// @ts-expect-error just keep backbone implementation as is
options = this._pending;
this._pending = false;
this.events.modelChanged.dispatch({
object: this,
options: options
});
}
}
this._pending = false;
this._changing = false;
return this;
};
AcCmObject.prototype.has = function (key) {
return this.get(key) != null;
};
/**
* Determine if the model has changed since the last `"change"` event.
* If you specify an attribute name, determine if that attribute has changed.
*/
AcCmObject.prototype.hasChanged = function (key) {
if (key == null)
return !isEmpty(this.changed);
return has(this.changed, key);
};
/**
* Return an object containing all the attributes that have changed. Useful for determining what parts
* of a view need to be updated and/or what attributes need to be persisted to the server.
*
* Unset attributes will be set to undefined. You can also pass an attributes object to diff against
* the model, determining if there *would be* a change.
*/
AcCmObject.prototype.changedAttributes = function (diff) {
if (!diff)
return this.hasChanged() ? clone(this.changed) : {};
var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
for (var attr in diff) {
var val = diff[attr];
if (isEqual(old[attr], val))
continue;
changed[attr] = val;
}
return changed;
};
/**
* Get the previous value of an attribute, recorded at the time the last `"change"` event was fired.
*/
AcCmObject.prototype.previous = function (key) {
if (key == null || !this._previousAttributes)
return null;
return this._previousAttributes[key];
};
/**
* Get all of the attributes of the model at the time of the previous `"change"` event.
*/
AcCmObject.prototype.previousAttributes = function () {
return clone(this._previousAttributes);
};
/**
* Create a new model with identical attributes to this one.
*/
AcCmObject.prototype.clone = function () {
var attrs = clone(this.attributes);
return new AcCmObject(attrs);
};
return AcCmObject;
}());
export { AcCmObject };
//# sourceMappingURL=AcCmObject.js.map