Repository navigation
matreshka 3.0.0
3.0.0 is a new major version: one package, written in TypeScript, ES modules only. The API of 2.x stays; the changes below say what moved and what was removed.
Added
- An agent skill for AI coding agents:
npx skills add finom/matreshka. - Types for the whole API, generated from the TypeScript source. The types a custom binder or handler needs (
Binder,BindNodeOptions,EventOptions,Mediatorand others) are exported frommatreshka. - Named exports:
import { bindNode, calc, MatreshkaObject, MatreshkaArray } from 'matreshka', next to the defaultMatreshka. - The router and the form parser are part of the package:
import { initRouter, Router } from 'matreshka/router'andimport { parseForm } from 'matreshka/parse-form'. The script-tag bundle has them asMatreshka.initRouter,Matreshka.RouterandMatreshka.parseForm. - The binders that came from the
common-binderspackage are part of Matreshka (Matreshka.binders,import { binders } from 'matreshka'). dist/matreshka.min.mjs: the whole library as one minified ES module, the router and the form parser included, for<script type="module">pages that load it from a CDN."sideEffects": falseinpackage.json, so a bundler drops the parts an app doesn't import.Matreshka.Arrayhasat,findLast,findLastIndex,flat,flatMap,toSorted,toReversed,toSplicedandwith; its iterators are iterable, and a nativeconcatspreads it.
Changed
- ES modules only.
require('matreshka')works on Node.js versions that load ES modules withrequire(22.12 and later) and gives the module's exports (const { Matreshka } = require('matreshka')). - The entry points are
matreshka,matreshka/routerandmatreshka/parse-form; the deep paths of 2.x (matreshka/bindnode,matreshka/array/from) are gone. - One script-tag bundle,
dist/matreshka.jsanddist/matreshka.min.js(also on jsDelivr and unpkg), with the router and the form parser in it. $nodesand the result ofselectAlland$are arrays of nodes. 2.x returned a collection of its bundled jQuery-like library, or of jQuery when it was on the page.- A change that comes from a node isn't written back into that node while the node shows that value: clearing a field whose value a mediator turns into
0leaves it empty, and the other nodes bound to the property show0. A value the mediator changed in another way (clamped, upper-cased) is written back, and the caret stays where it was. - The code targets current browsers and Node.js; it is no longer compiled to ES5. It imports without a DOM (server-side rendering, Node.js, test runners without jsdom).
off()with no arguments removes only the listeners added withon,onceandonDebounce; bindings,calcdependencies and the built-in events ofMatreshka.ObjectandMatreshka.Arraykeep working. It firesremoveevent.undefinedandnullshow as empty in the binders and in parsed{{key}}attributes, as they already did in parsed text.Matreshka.Class: a class without its own constructor calls the parent constructor with the same arguments, and a constructor written as a method (constructor() {}) or an ES class works.copyWithinon a rendered array throws, likefill, instead of leaving the DOM out of sync.new Matreshka.Array(n)throws aRangeErrorfor an invalid length, likenew Array(n).- A property that can't be redefined (of a frozen or sealed object, or an array's
length) throws an error that names it when it is bound, listened to or used as acalcsource. sortwith a comparator that is not a function throws aTypeError, like the native one.
Removed
- The packages
matreshka-routerandmatreshka-parse-form: usematreshka/routerandmatreshka/parse-form. Matreshka.useDOMLibraryand the use of a global jQuery or Zepto. Matreshka works with the DOM directly;bindNodestill accepts a jQuery object, or anything array-like, as nodes.- The
MKglobal alias of the script-tag bundle. - The misspelled
Matreshka.parserBrackers; the name isMatreshka.parserBrackets.
Fixed
Events
off(name, callback)removes DOM event handlers (click::x), also from nodes bound later, and keeps the other DOM events and the user's ownbind:KEYlisteners.once()on a DOM event fires only once, and aonce()handler that fires its own event again runs only once.- The same DOM event handler added twice is called once.
- Several delegated listeners on the same path (
a@change:x,a@change:y) all follow a reassigned branch, and removing one delegated or*@listener keeps the others. - Replacing a data value of a
Matreshka.Objecttakes*@listeners off the old value. off()on an object without listeners no longer throws;off()on one object no longer misses its DOM handlers on a node another object also listens on.- Adding or removing a DOM event listener fires
addevent:NAME/removeevent:NAMEunder its own name; names likeaddevent:click::xare no longer treated as DOM events. - Delegated
focus::KEY(selector)andblur::KEY(selector)work with the built-in DOM library. parseHTMLandparseBindingshandle<col>.off()with no arguments stays fast with many listeners.- Delegated and
*@listeners keep working on a child that another data key, property or array slot still holds after one of them is replaced or removed. - A function added both as a listener and as a delegated listener (
*@foo) on the same object stays until both are removed. - DOM events (
click::x) reach a node bound withsilent: trueand leave a node unbound withsilent: true, so items an array renders silently get their DOM events. - A delegated DOM event's selector may contain commas inside
:is(),:not()and attribute values (click::x([data-k="1,2"])). - Delegated listeners (
a.b@change:c) keep working when a branch is replaced by a copy that shares the next object (obj.a = { ...obj.a }). onceremoves only its own listener, not the same function added withon.offcancels a pendingonDebouncecall, andoff(null, callback)removes that callback from every event.- Delegated DOM events work on
documentand for events that don't bubble (mouseenter,mouseleave,load,scrolland others). - A delegated DOM event's selector may hold a space before a pseudo-class with parentheses, or
@in an attribute value. - A trailing space in event names adds no listener with an empty name.
removetakes off the DOM listeners of the key, andremoveof acalctarget stops the calculation.removeeventfires after the listener is gone: aremoveevent:xhandler that callsoff('x')no longer loops until the stack overflows, and a listener it adds toxstays.- A listener that several delegated paths reach (
a@xandb@xon one shared object,selected@xanditems.*@x, two deepcalcsources through one object) stays until the last of them is removed;off,removeand a changed branch no longer take it from the others. off(root, 'c@x', f)keeps the same delegated listener that another root added to a shared child.on(name, f)afteronce(name, f)oronDebounce(name, f)adds a listener of its own.off(null, callback)also removes the callback from delegated events.off()on an object keeps the listeners a parent delegated to it, so the parent can add them again.offof one name of anonDebounceon several names keeps the pending call of the others.
Bindings
- Binding a radio group keeps the radio that is already checked; an unchecked radio no longer sets the value.
unbindNode(key, node)keeps the other nodes bound to the key, andnodes/$nodesstay correct.unbindNodeof a deep key (a.b.c) stops the binding for good, keeps other deep bindings on the same branch working, doesn't throw when the branch is gone, and honoursexactKey;unbindNode(null)also removes deep bindings.- After
unbindNode, debounced updates still pending no longer reach the node or the property. - The select binder selects the option "0" for the value 0; a
multipleselect accepts null and numeric values. binders.existence()bound to several nodes hides and restores all of them.binders.style()reads camel-cased properties from the computed style.- Parsed attributes show values containing
$&and$$literally, and custom parser brackets may contain regexp characters. - Typing a value that a mediator turns into a number no longer writes it back into the same field.
- Selectors with
:sandbox/:bound()support commas inside:is(),:not()and attribute values. - A failed
bindOptionalNodecall no longer makes the nextbindNodeoptional. select(':sandbox')and$(':bound(x)')on an object without bindings return an empty result instead of throwing.sandbox(andcontainerof aMatreshka.Array) no longer appear inObject.keysorJSON.stringify.binders.style()works with dashed and custom property names;null,undefinedor''removes the property.selectAll(':sandbox, .foo')on an object without bindings still returns the.foonodes.- A binder's
setValueandgetValueget the current value inoptions.value, not the value at bind time. - Custom binders can be added to
Matreshka.binders(Matreshka.binders.myBinder = …), and its type allows them. - Input typed into a node before a debounced write from code lands is kept: the older write is dropped.
- A deep binding (
a.b.c) keeps working when a branch is replaced by a copy that shares the next object, andremove('a')removes the deep bindings undera. - Unbinding a node whose binder has no
setValuekeeps the bindings of the key's other nodes. - A value set again inside a change handler: deep bindings and delegated listeners follow the value it ends with.
- A bound value that holds
{{…}}is not parsed as a binding by a laterparseBindings. unbindNodeof a deep key without a node keeps the bindings made on the target directly;unbindNode(null)removes bindings made withexactKeyto a dotted key;unbindNode({ key: null })unbinds the key.binders.style()clears a camel-cased property onundefined.triggerandselectAllwith a selector skip the text and attribute nodes bound byparseBindings.- A property's own setter keeps running after the property becomes reactive: it gets each new value, and the property's getter gives the value the events and bindings see.
- The radio binder checks the radio whose value equals a number, as the select binder does.
- Parser brackets containing
<,>,&or"bind text and attributes. binders.existence()on a form field no longer takes the field's text as its value.value="{{x}}"of a submit, reset, button or image input binds as an attribute.parseBindingswith an HTML string binds{{key}}in its top-level text.- A refused second
sandboxorcontainernode no longer appears in$nodes. bindNodewith a binder no longer changes the binder adefaultBindersfunction returns.selectandselectAllwith a list such as.y, :bound(k)search the sandbox for the plain parts.- A keyup or click that leaves a checkbox as it was no longer undoes a value the program has just set.
- A node bound in an
unbindhandler stays bound. - A
silentdeep binding no longer firesunbindon the old branch when the branch changes.
Data
calcwith a deep source (a.b.c) no longer throws when a middle object becomesnull, and calculated properties on different objects no longer block each other.skipMediator,force,forceHTMLandsilentHTMLon a source no longer apply to the calculated target.- With
promiseCalc, the latest calculation wins over slower earlier ones, and a rejection reports the original error. - A synchronous
calcthat two others depend on (a diamond:bfroma,canddfromb,efromcandd) recalculates the end with both new values. skipCalcandforceon a deep source (a.b.c) work as on a plain one, and a deep source keeps working when a branch is replaced by a copy.removereturns the object, as documented, and takes delegated listeners and deepcalcdependencies off the removed value.removefiresdeleteanddelete:KEYfor a property Matreshka doesn't track yet.toMatreshkatreats only arrays as arrays (a plain object with alengthproperty becomes aMatreshka.Object) and copies only the data keys of aMatreshka.Object.- Assigning a
Matreshka.Objectinstance to an instantiated property copies only its data keys; assigning{}clears it. Matreshka.Object:remove(key, { silent: true })also removes the data key; keys named liketoStringwork as data keys;addDataKeysandremoveDataKeysmove*@listeners;removeDataKeysfiresremovebeforemodify;off('change')no longer breaks itsset,modifyandremoveevents.- Instances made by
Matreshka.Classno longer listconstructorinfor...in. Matreshka.Classkeeps getters and setters of the prototype as accessors (before, a getter ran once atClass()and its value was copied), and instances no longer listextendsinfor...in.- A key named
__proto__in data passed tosetData,Matreshka.Objector a tracked array item no longer writes ontoObject.prototype. setDataof the value a key already holds makes it a data key with its*@listeners and firessetandmodify; withreplaceDataandsilentit fires noremoveormodify.new Matreshka.Object(instance)andsetData(instance)take only the instance's data keys.- A setter declared in a class keeps running after the property becomes reactive.
Matreshka.Object#toJSONkeeps a data key named__proto__.- A path through
__proto__(calc(obj, 'a', '__proto__.x', fn),on(obj, '__proto__@change:x'),bindNode(obj, '__proto__.x', node)) no longer defines its key onObject.prototypeor a class prototype;set(obj, '__proto__', value)writes an own key.
Matreshka.Array
splice(start)with one argument removes all items fromstart, and passes the real index of each added item toModeland the item mediator.- A rendered item can be added again after it was removed (
push,unshift,splice, filtered views withrecreate, moving to another array). ModelanditemRendererdeclared as class fields work.trackBykeeps a tracked item when it moves to an index the old array didn't have.- Rerendering releases every binding to the old item nodes, deep (
a.b) and exact-key ones included, and no longer throws when an item is also bound towindowor another non-DOM target. - Changing
Modelon a rendered array rerenders the converted items and runs the render hooks (onRender,onItemRender,render,afterrender). pullwith a negative or fractional index removes nothing, removing anundefineditem fires the events, andpullreturnsnullwhen nothing is removed.fillappliesModeland the item mediator, honours start and end, and reports the replaced items asremoved.Matreshka.Array.ofandMatreshka.Array.fromapply the subclass'sModel.spliceinserts a new item's node in the right place when a primitive sits next to it, and removing an item no longer throws when its node is outside the current container.spliceputs new nodes in the right place when the last added value appears again later in the array.- Removing an item hidden by the
existencebinder also removes its placeholder comment. recreate()with the same items in another order moves their nodes into the new order.recreate(null)empties the array, andnullassigned to a property instantiated as aMatreshka.Arrayclears it.trackByworks with ids named likeObject.prototypemembers, and aModelinstance passed as new data no longer addsnodesand$nodesas data keys of the tracked item.- A
splicethat puts back an item it removes keeps the item's node instead of throwing. orderBysorts items without the key, andnullitems, after the others (before them withdesc), as lodash does.indexOf,lastIndexOfandincludestake the same arguments as the array methods (lastIndexOf(x, undefined)).recreatetakes an array-like on a non-empty array, and reports a dropped duplicate as removed.copyWithintakes*@listeners off the overwritten items;fillover an empty range fires nomodify;push_(),unshift_()andsplice_()with no items no longer throw.- Rerendering also releases the bindings of parsed attributes (
title="{{a}}!") and their calculations. - A
Modelthat extendsMatreshka.Arraygets the item's data as its items, so a collection of collections works and converts to JSON. - Items are rendered before
add,pushand the other events fire; an array changed from such a handler keeps the DOM in its order. from,ofand a silentrecreatekeep*@listeners in step with the items.silent: truealso silenceschange:length.addoneandremoveoneget the event options passed to theMETHOD_call.restore()on an array with an item mediator links the restored nodes to the mediated items.- After
itemRenderer = null, sorting no longer brings the old nodes back. - A forced rerender removes the old nodes from a container the array no longer uses, and leaves a node the user moved elsewhere.
unshiftkeeps holes, as the native method does.orderByputsNaNlast, as lodash does.String(array)andtoLocaleString()list the items, as for an ordinary array.
Router
- The back button updates the properties when it returns to the first page entry or to an entry the router didn't push.
- The history router keeps the query string when it changes the path.
- Values with spaces, non-Latin letters or slashes are encoded in the URL and decoded back.
- A property that is
nullor''when it subscribes gets its value from the URL. - A plain anchor such as
#topis no longer read as a route. - Subscribing a shorter route no longer drops the deeper parts of the URL.
router.partsno longer holdsundefinedentries, and three or more slashes in a row are treated like one.- Opening a hash route that differs from the router's path only by its trailing slash no longer adds a history entry.
0is a route value, not an empty part.- Subscribing to the hash router keeps a plain anchor (
#section) in the URL. - Opening a hash route spelled another way (
@unescaped, lowercase escapes) no longer adds a history entry.
Form parser
- When the form isn't found,
parseFormreturnsnulland binds nothing; before, it bound every field of the document. - A wrong first argument throws
parseForm's ownTypeError. parseFormbinds every field of a form, including<input type="image">and fields tied to it with theformattribute.- A file input bound to an object that already has a value no longer throws.
parseFormbinds a named<progress>.
Types
thisin the prototype passed toMatreshka.Classhas the parent's methods.- An event handler may use
this(the context given toon). new Matreshka.Array('Hi', { a: 'b' })with items of different types;MyArray.fromandMyArray.ofgiveMyArray.itemRenderermay be a method, a getter or a field.new Router('custom')makes a router of its own type.- The items of
unbindNode([{ key }])may leave out the node.