Ornament now has a sort-of proper web page! The page is generated straight from the readme file and does not add much.
Two functions have received backwards-compatible updates to make them work in
environments where the window
object is not the global object. This is only
relevant if you run your component code in eg. Node.js with
JSDOM or similar to do SSR. The affected
functions are:
- decorator
@define()
: now takes an optional third argument to customize whichCustomElementRegistry
to use. Defaults towindow.customElements
. - transformer
href()
: now takes an optional options object with a fieldlocation
that can customize theLocation
object to use. Defaults towindow.location
.
This will be useful if you want to run your components outside of browsers and of no concern if you don't.
You can now control if an invocation of an observer callback should cause an invocation of the decorated method or class field function:
import { define, observe } from "@sirpepe/ornament";
@define("my-test")
class Test extends HTMLElement {
@observe(MutationObserver, {
childList: true,
// Only call the decorated method when the records contain removals
predicate: (element, records, observer) => {
const removals = records.filter((r) => r.removedNodes.length > 0);
return removals.length > 0;
},
})
reactToUpdate(records, observer) {
console.log("Something happened!");
}
}
const instance = new Test();
document.body.append(instance);
const el = document.createElement("div");
instance.append(el); // no nodes removed = no output
// Wait some time (mutation observers batch mutations)
el.remove(); // el removed = "Something happened!"
This makes it more feasible to combine @observe()
with @reactive()
on
methods that need to react to changes but that should not be overburdened with
figuring out whether or not the root cause is actually cause for a reaction.
This work belongs to the decorators, and has always been supported via a
predicate in the options for @reactive()
. Now @observe()
can do the same!
Ornament, being a collection of decorators, now stores its metadata in Decorator Metadata. This gets rid of a few janky workarounds and saves a few bytes in the process, but is not really noticeable in and of itself. What is actually new is an API to access (some of) the available component metadata:
import {
define,
attr,
string,
number,
getTagName,
listAttributes,
getAttribute,
} from "@sirpepe/ornament";
@define("my-test")
class Test extends HTMLElement {
@attr(string()) accessor foo = "";
@attr(number({ min: 0 }), { as: "asdf" }) accessor bar = "";
}
console.log(getTagName(Test)); // > "my-test"
console.log(listAttributes(Test)); // > ["foo", "asdf"]
const { prop, transformer } = getAttribute(Test, "asdf");
// prop = "bar" = name of the public accessor for the content attribute "asdf"
// transformer = the transformer for the content attribute "asdf"
transformer.parse("-1");
// > 0; input clamped to valid value
transformer.validate(-1, true);
// Throws an error; the transformer only accepts nonnegative numbers
This should be useful if you need access to the parsing and stringification logic for content attributes to do eg. SSR.
Bump dependencies, play a little code golf, tweak and expand readme, run tests against playwright's "webkit" browser too - for whatever that's worth.
@subscribe()
no longer has support for mapping an event to a specific matching event subtype (like "click"
to MouseEvent
). This feature was added in 1.2.0 and required supplying up to 5 type parameters, which was extremely clunky and it did not even work all that well. The core of the issue is that @subscribe()
can subscribe to any event target, and any event target can in principle dispatch any event. The only way to truly solve this is to build abstractions for specific use cases:
// Subscribes to DOM events in particular
const listen = <T extends HTMLElement, K extends keyof HTMLElementEventMap>(
source: HTMLElement,
...eventNames: K[]
) =>
subscribe<T, HTMLElement, HTMLElementEventMap[K]>(
source,
eventNames.join(" "),
);
The decorator listen()
can only handle event targets that are DOM elements, but can also better constrain its input.
The new transform
option for @subscribe()
enables methods to accept data that has been computed from an event object or signal value, rather than the event object or signal value itself:
const counter = signal(0);
@define("test-element")
class Test extends HTMLElement {
// Subscribes to the signal, transforms every new value into a String
@subscribe(counter, { transform: (_, v) => String(v) })
test(value: string) {
console.log(value);
}
}
const instance = new Test();
// > logs the string "0"
This essentially copies the last event or signal value straight into the class, triggering methods decorated with @reactive()
in the process:
const target = new EventTarget();
@define("test-element")
class Test extends HTMLElement {
@subscribe(target, "foo") accessor test: Event | null = null;
@reactive() react = () => console.log(this.test);
}
const instance = new Test();
const event = new Event("foo");
target.dispatchEvent(event);
// Logs the object "event"
instance.test === event; // > true
Fix support for line breaks as separators in event strings for @subscribe()
and bump dependencies.
Ever wanted to have an element observe itself? The new decorator @observe()
registers a MutationObserver, ResizeObserver, or IntersectionObserver to use the decorated method as its callback. It then starts to observe the element for whatever the observer observes (resizes, mutations etc.):
@define("my-test")
class Test extends HTMLElement {
// Pass the observer constructor and relevant options
@observe(MutationObserver, { childList: true })
reactToChanges(mutations, observer) {
// "mutations" = array of MutationRecord objects
// "observer" = the observer instance
console.log(mutations);
}
}
const el = new Test();
el.innerText = "Test"; // cause mutation
// Test.reactToChanges() gets called asynchronously by the observer
Similar to @subscribe
you can decide when/if observation should start and end in the options, which are also the options for the observers.
Bump dependencies, re-organize tests, use an eslint/prettier setup that matches the current century, and add some more hype to the readme.
@subscribe()
now lets you decide when/if a component should subscribe and unsubscribe from an event target or signal. It defaults to the internal "init"
and "connected"
events for subscribing and to disconnected
for unsubscribing, but this can be changed in the options when calling @subscribe()
.
TypeScript can now verify whether methods that get subscribed to EventTargets via @subscribe()
expect the right type of event. This only works if a mapping between event names and event object types for the event target exists (such as HTMLElementEventMap
) and if that mapping, along with several more types, gets passed to @subscribe()
as type parameters. This is by itself very inconvenient, but can be made bearable by building abstractions on top.
Mention the fact that @subscribe()
can listen to more than one event in the docs, and bump dependencies.
@subscribe()
can now also take promises for event targets and promise-returning factories as its first argument.
Ornament now ensures that all methods decorated with @init()
in all classes in an inheritance chain only fire when the last enhanced constructor finishes. Previously, methods decorated with @init()
fired when their specific constructor finished, or not at all if the class was not decorated. This ensures a consistent behavior for more convoluted inheritance chains.
Bump dependencies, play some code golf, and tweak error messages.
Methods decorated with @reactive()
can no longer run on init. The initial
option has been removed. Use @init()
instead.
The new decorator @init()
runs methods and class field functions on instance initialization (that is, when the constructor has completed).
The decorators @connected()
, @disconnected()
, @adopted()
, @formAssociated()
, @formReset()
, @formDisabled()
, @formStateRestore()
, @subscribe()
and @reactive()
now also work on class field functions. The decorator @debounce()
now also works on static methods and static class field functions.
Fix @connected()
throwing on private methods when an already-connected component initializes.