5.0.0-beta.1 · Published beta · b06750c5. Published on npm. Match your installed version.
Consumer@mnjs/data#
Dependency-light observable Model and ordered Collection sources for
Marionette v5. The package depends only on @mnjs/utils; models and
collections can run without core or a DOM. Install @mnjs/data on its own
for standalone use. To use it with Marionette views, install both packages and
configure the runtime before creating owners:
npm install marionette@5.0.0-beta.1 @mnjs/data@5.0.0-beta.1
import { CollectionView, View } from 'marionette';
import { Collection, DataApi } from '@mnjs/data';
const Row = View.extend({
tagName: 'li',
template: () => '<span></span>',
modelEvents: { change: 'render' },
onRender() {
this.el.querySelector('span').textContent = this.model.get('label');
}
});
const List = CollectionView.extend({ tagName: 'ul', childView: Row });
Row.setDataApi(DataApi);
List.setDataApi(DataApi);
const collection = new Collection([{ id: 1, label: 'one' }]);
const view = new List({ collection }).render();
This setup selects data for the list and its child Views. State remains an
independent choice. If a View also uses a Model as observable state, configure
StateApi on that class before construction. In the example above, place this
optional setup before new List(...), which constructs its children when rendered:
import { StateApi } from '@mnjs/data';
Row.setStateApi(StateApi);
Supply an existing Model through state, or return an owned one from
createState(). Declare stateEvents only for the changes the owner needs to
observe; the model's event names and payloads remain its own contract.
Use top-level setters when all affected classes intentionally share the same provider. Configure an existing isolated runtime through its corresponding setters when needed; using this package does not require creating a new runtime.
Collection reports synchronous kind: 'update', kind: 'reorder', and
kind: 'reset' records through DataApi.observeCollection(). Model and
Collection expose on(), once(), off(), trigger(), and
triggerMethod() for Marionette entity event maps.
DataApi.models(collection) returns the current ordered model snapshot.
Model provides get, has, set, unset, clear, reset, toObject, and
destroy. Collection provides ordered at, get, indexOf, iteration,
forEach, map, add, remove, reset, move, sort, toArray, and destroy operations. Pass { silent: true } to a
structural mutation to suppress its normalized record and entity events.
destroy() is the exception and always emits its destruction event.
Define subclass defaults on the prototype, for example with Model.extend, a
prototype method, or a prototype getter. Native class fields initialize after
super() returns, so a defaults = { ... } field cannot seed construction.
move(modelOrId, index) changes list order without removing and re-adding a
model. This supports drag ordering while retaining child Views and their local
state. Both move and sort emit sort, translated to a DataApi reorder record.
Update model attributes with model.set() and subscribe through modelEvents
when a child should render after a change.
The native DataApi uses each model's stable cid as its key. Application ids may
change; Collection lookup reads the current ids. Duplicate instances or ids are
rejected before a reset changes membership; add ignores an instance or id
already present. Applications should keep ids unique when changing them.
get, remove, and move resolve an exact member instance first, then an
application id, then a cid. This precedence does not change when models move.
Bulk removal resolves its inputs against one current membership snapshot, including
silent id changes. It skips missing identities and repeated matches, returns
removed Models in input order, and keeps surviving Models in collection order.
If id writes temporarily create duplicates, id lookup selects the first current
member; applications should restore unique ids.
Supplied native Model instances retain their identity, attributes, and subclass,
including when the Collection has a different model constructor. That constructor
is used only for raw attribute objects. Initial model instances do not configure
the constructor used for future raw additions.
A model may belong to multiple Collections. Its destroy event removes it from
each containing Collection, forwarding removal options such as silent.
The destroy event itself still fires. Destroying a Collection releases subscriptions; it
does not destroy its models.
model.toObject() returns a shallow attribute copy. collection.toArray() returns
an array of those plain objects; use collection.models.slice() or iteration for
model instances. Template serialization reads model.attributes independently of
these conversion methods. There is no automatic toJSON hook: to serialize the
plain data, use JSON.stringify(model.toObject()) or
JSON.stringify(collection.toArray()).
Collection observation uses ordinary synchronous update, reset, and sort
events. Notifications are not combined or replayed. Complete one structural
mutation before starting another: schedule mutations from collection listeners
or child lifecycle handlers after the current notification returns. Errors in
listeners propagate and abort delivery, as with ordinary model events.
The package does not provide persistence, REST synchronization, validation, or implicit Backbone compatibility.
Mutation semantics#
set compares values with Object.is: a fresh object is a change even when its
contents match, while mutating a nested object in place is not observed. has
tests own-property presence, including a present undefined or null value.
Supplied attributes override defaults, including when their value is undefined.
Model reset reapplies defaults and removes attributes absent from the result.
Change callbacks receive options.changed and options.previous, sparse maps for
that mutation. For an attribute reported in changed, an absent own key in
previous means it did not exist before the mutation; an own key with value
undefined means it existed with that value. previous is
not a complete model snapshot. Removing an attribute reports undefined in
changed; use has to check its current presence.
Nested Model writes complete synchronously as independent changes. Use the event's
options.changed to inspect that event: model.changed reflects the latest write,
which may be a nested mutation by the time an outer change callback runs. Silent
writes still update attributes and changed; no-op writes clear changed.
Collection add and remove events originate on the Collection. Model events are
forwarded by containing Collections. Sorting is explicit: a prototype comparator
is used by sort(), but add and reset do not automatically sort. There is no
Collection.set() merge/reconcile operation; update retained Models explicitly
when refreshing a list whose child Views must retain local state. reset is the
deliberately destructive whole-list operation for CollectionView child Views;
the Collection retains supplied Model instances rather than destroying them.
TypeScript#
The package includes ESM and CommonJS declarations and a TypeScript 4.6-compatible
entry. Model.extend and Collection.extend retain added methods, descendants,
static replacements, and their normal attribute/model constructor inference.
Event registration accepts typed callbacks and maps; event names do not validate
payload types. A borrowed triggerMethod requires a receiver with a callable
trigger method.
A custom constructor must initialize the receiver itself. An explicit object return describes a replacement instance; an unknown result stays unknown. To return the initialized receiver while preserving methods added by descendants, state that contract explicitly:
import { Model } from '@mnjs/data';
const Named = Model.extend({
constructor: function<Receiver extends Model>(
this: Receiver, attributes: { label: string }
): Receiver {
Model.call(this, attributes);
return this;
},
label() { return String(this.get('label')); }
});
The same form works with Collection. A constructor declared to return void
or a primitive declares ordinary construction; the caller is responsible for
honoring that declaration. TypeScript's void return erasure can hide an object
return, so the declarations cannot prove that contract from arbitrary constructor
implementations. An inferred fixed receiver return does not promise methods
added by later descendants.
Direct native subclasses remain supported. Calling their inherited .extend()
without an explicit constructor is rejected because that path calls the parent
with apply, which cannot invoke a native class. An explicit constructor skips
that forwarding path and owns its initialization or replacement result.
TypeScript 4.6 narrows instanceof checks for the root constructors and ordinary
method-only extensions. Its callable-intersection limitation prevents that
narrowing on extensions with custom statics; directly constructed instances and
those static members remain typed.
Collection member types include both supplied Models and the constructor used for
raw attributes. Constructor options.model replaces a prototype model factory;
without either, raw attributes construct a base Model. Narrow an item with
instanceof ModelClass before using subclass-specific methods. The instance
model constructor has the same conservative member result type.
Model attributes and toObject() are partial: construction, unset, and clear
can leave any attribute absent. Known string keys in set(key, value) use the same
attribute value types as object-form writes; arbitrary dynamic keys remain open.
An explicitly typed Collection also checks raw attribute inputs against its model
attribute shape. These are compile-time contracts, not runtime validation.