The power of lit-html with the familiarity of native Web Components
Register a callback to a class property setter.
This allows us to hook into state changes of an object
import { observeProperty } from 'fam-element'
class Foo {
constructor(id) {
this.id = id
}
@observeProperty((instance, oldValue, newValue) =>
console.log(`${instance.id}'s message set from ${oldValue} to ${newValue}`)
)
message?: string
}
const bar = new Foo('bar')
bar.message = 'Hello!'
// console: bar's message set from undefined to Hello!
The decorator will always return a property descriptor, so if you don't want to use typescript, you can use the return value to define properties
class Foo {}
Object.defineProperty(
Foo.prototype,
'message',
observeProperty(() => console.log('message updated'))(Foo.prototype, 'message')
)
Register update callbacks to a class and request updates against an instance of that class. If an update is requested multiple times in the same tick, the update callbacks will only be called once.
This enables us to run expensive operations as a result of some state change of an object, regardless of how many times the object's state changes in a tick.
import { onUpdate, requestUpdate } from 'fam-element'
@onUpdate(instance => console.log(`Hello ${instance.message}`!))
class Foo {
constructor(message) {
this.message = message
}
}
const bar = new Foo('World')
requestUpdate(bar)
// next tick - console: Hello World!
The decorator is designed to apply logic to the existing class instead of wrapping it, so if you don't want to use typescript decorators, you can simply pass your class into the decorator method.
class MyUpdateableClass {}
onUpdate(() => {
/* do work */
})(MyUpdateableClass)
const myUpdateableObject = new MyUpdateableClass()
Register change callbacks to properties of a class. If any watched property is changed, all changed property callbacks will be called in the next tick.
This is similar to observeProperty, except the callback will be executed in the next tick and will only fire if the property actually changes instead of just being set.
import { onPropertyChange } from 'fam-element'
class Foo {
@onPropertyChange((instance, { oldValue, newValue }) =>
console.log(`bar changed from ${oldValue} to ${newValue}`)
)
bar?: string
}
const myFoo = new Foo()
myFoo.bar = 'baz'
// next tick - console: bar changed from undefined to baz!
By default, a change is detected by checking exact equality. This can be configured by registering a custom change detector for the property
import { onPropertyChange, changeDetector } from 'fam-element'
@onUpdate(instance => console.log(`Hello ${instance.message}`!))
class Foo {
@changeDetector((oldValue, newValue) => oldValue.toUpperCase() !== newValue.toUpperCase())
@onPropertyChange((instance, { oldValue, newValue }) => {
/* this will only be called if bar changes without considering case sensitivity */
})
bar: string = 'baz'
}
onPropertyChange will always return a property descriptor, so if you don't want to use typescript, you can use the return value to define properties
class Foo {}
Object.defineProperty(
Foo.prototype,
'message',
onPropertyChange(() => console.log('message updated'))(Foo.prototype, 'message')
)
changeDetector modifies metadata on the class, so all you need to do is pass the target and property key into the decorator. This will always be void so don't assign it through Object.defineProperty
changeDetector((oldValue, newValue) => oldValue != newValue)(Foo.prototype, 'message')
Register an update callback to a subset of properties on a class. This will wait for all registered properties to updatebefore calling the callback in the next tick.
This allows you to perform specific operations against the instance of a class when those operations are dependent on certain properties.
import { updatePipeline } from 'fam-element'
const myPipeline = updatePipeline((fooInstance, pipelinePropertyChangeStates) =>
console.log(`Hello ${fooInstance.name}!`)
)
class Foo {
@myPipeline.registerProperty
name: string
constructor(name) {
this.name = name
}
}
const myFoo = new Foo('World')
// next tick - console: Hello World!
It's also possible to manually trigger a pipeline to run regardless of a property updating
myPipeline.requestUpdate(myFoo)
// next tick - console: Hello World!
registerProperty will always return a property descriptor, so if you don't want to use typescript, you can use the return value to define properties
class Foo {}
Object.defineProperty(Foo.prototype, 'name', myPipeline.registerProperty(Foo.prototype, 'name'))
Observe HTMLElement attributes with decorators
import { observeAttribute } from 'fam-element'
@observeAttribute('my-attribute', (instance, oldValue, newValue) =>
console.log(`my-attribute changed from ${oldValue} to ${newValue}`)
)
class MyCustomElement extends HTMLElement {}
is identical to
class MyCustomElement extends HTMLElement {
static get observedAttributes() {
return ['my-attribute']
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'my-attribute') {
console.log(`my-attribute changed from ${oldValue} to ${newValue}`)
}
}
}
observeAttribute will mutate the existing class instead of wrap it, so it can be called after the class is declared
class MyCustomElement extends HTMLElement {}
observeAttribute('my-attribute', (instance, oldValue, newValue) =>
console.log(`my-attribute changed from ${oldValue} to ${newValue}`)
)(MyCustomElement)