data-double-dash v1.21.3
What is it ?
data-double-dash
is a great front-end solution for MPA applications using templating engines (such as handlebars, twig, jinja, etc) or plain HTML. In that context, it is designed to enable component-based development in a type-safe manner while staying as light as possible (< 8kb minified + gzipped).
Example
The following example shows how to create a simple counter :
ddd({
// The name of the component, any DOM element with a "data--counter" attribute will now be considered a component of type "counter".
name: "counter",
// A schema to validate a component's props
props: ddd.object({ step: ddd.number() }),
// The component's initial state
state: { count: 0 },
// The component's event handlers
handlers: {
increment(event, { element, props, state }) {
state.count += props.step // increment state's count
element.textContent = state.count.toString() // update UI
},
},
})
<button
data--counter='{ "step": 1 }'
data--counter--click='{ "handler": "increment" }'
>
0
</button>
<!--
- `data--counter='{ "step": 1 }'` :
Mount a "counter" component with the following props '{ "step": 1 }'.
- `data--counter--click='{ "handler": "increment" }'` :
Add an event listener for the "click" event and use the "increment" method as its handler.
-->
Installation
# npm
npm install data-double-dash
# yarn
yarn add data-double-dash
# pnpm
pnpm add data-double-dash
Manager
The data-double-dash
manager is a singleton which monitors DOM changes and is responsible for registering and unregistering templates and mounting and unmounting components.
The manager works asynchronously by default, in other words, when you register a new template or add a new element to the DOM, its corresponding component will only be mounted at the end of the current tick. The same is true when unregistering a template or removing an element from the DOM. You can force a synchronous synchronization by executing ddd.sync()
, this is not forbidden or bad practice, but it should be avoided when possible for performance reasons.
It shouldn't be needed but if you need to, you can stop the manager from monitoring DOM changes with ddd.unwatch()
. This will make the manager not mount new elements added to the DOM and not unmount elements removed from it (causing possible memory leaks). To re-monitor the DOM use ddd.watch()
.
Components
Components are DOM elements that have been enhanced with the functionality defined in one or more component templates. Essentially, a component is an interactive DOM element managed by data-double-dash
. The main way to interact with a template's component is from within the template, for example in one of its handlers or in a lifecycle method. Every component is a self-contained object with the following properties: template
, id
, element
, props
, refs
, state
, listener
, dispatch
, timeout
and interval
.
template
: The template property is a string corresponding to a component's template name.id
: Every component has a unique numeric string ID. When the component is mounted, DOM elements having either a component, an event or a ref attribute will also have an ID attribute consisting of its ID prefixed by"data--"
. For example for an ID"1234"
the attribute would bedata--1234
.element
: A component's element property is a reference to the root element for that component. A root element is the element which contains the template name attribute.props
: The props object passed to the component.refs
: A resolved object of the templates refs. Refs will be accessible by name, their kebab-cased name will be accessible as a camelCase name. For example if you declared a"foo-bar"
ref in the template, it will be accessed as"fooBar"
.state
: The state of your component, it is initialized with the state given in the template. You can put your component properties and methods here.listener
: An object containing two methods:add
andremove
, these methods can be used to create component events programatically. This can be useful when creating headless components (components that don't need HTML), such as a component that detects a click outside the current element and dispatch a custom"click-outside"
event.dispatch
: This method can be used to makeelement
dispatch a custom event.data-double-dash
's custom event bubble by default, so ancestors can listen to this event.timeout
: This is the same as usingsetTimeout
, except this timeout will be attached to your component, meaning that it will automatically clear itself when the component is unmounted. Its return value is a function to clear itself.interval
: This is the same as usingsetInterval
, except this interval will be attached to your component, meaning that it will automatically clear itself when the component is unmounted. Its return value is a function to clear itself.
A component is declared in your HTML with an attribute consisting of its template name prefixed by "data--"
, for example data--counter
. When declaring a component like that you can also pass props as JSON using the attribute value, for example data--counter='{ "step": 10 }'
. Props will only be available for that component instance.
Component events
An event can be declared by adding an attribute to the element component or one of its descendants. The attributre structure is as follows: "data--"+templateName+"--"+eventTarget+"."+eventType
or "data--"+templateName+"--+eventType
if the eventType
is the element where the event was added. For example the data--counter--window.click
is an event on the counter component listening for "click"
on window
, whereas data--counter-click
is an event on the counter component listening for click on the element where that attribute is. Available event targets other than none (self) are: "window"
(window
global), "document"
(document
global), "root"
(<html>
element) or a template name (will target the closest element that has such a component).
If you need to use an observer such as IntersectionObserver
, MutationObserver
or ResizeObserver
, you can listen to the "observeintersection"
, "observemutation"
and "observeresize"
event types instead. When trying to create an event for these event types the manager detects it, create the observer and emits these custom events
whenever their callback is executed. This enables you to use observers using handlers.
Just like component attributes can be given props as a value, event attributes can be given an option object as a value. The option properties are :
handler
: A string corresponding to the handler responsible to handle that event.params
: An object which will be given to the handler responsible to handle that event.dispatch
: A string corresponding to thecustom event's
type that this event will dispatch.detail
: An object which will be passed along with the dispatchedcustom event
.cancelable
: Whether the disaptchedcustom event
can be canceled, and therefore prevented as if the event never happened.composed
: Whether or not the disaptchedcustom event
will propagate across the shadow DOM boundary into the standard DOM.capture
: A boolean value indicating that events of this type will be dispatched to the registered listener before being dispatched to anyEventTarget
beneath it in the DOM tree.once
: A boolean value indicating that the listener should be invoked at most once after being added. If true, the listener would be automatically removed when invoked.passive
: A boolean value that, if true, indicates that the function specified by listener will never call preventDefault(). If a passive listener calls preventDefault(), nothing will happen and a console warning may be generated.observe
: A boolean indicating if observer event types should be detected to create their corresponding observer.root
: Used forIntersectionObserver
. A string selector (used withclosest
) which identifies theElement
whose bounds are treated as the bounding box of the viewport for the element which is the observer's target.rootMargin
: Used forIntersectionObserver
. A string with syntax similar to that of the CSSmargin
property. Each side of the rectangle represented byrootMargin
is added to the corresponding side in theroot
element's bounding box before the intersection test is performed.threshold
: Used forIntersectionObserver
. A number or an array of numbers representing percentages of the target element which are visible (intersection thresholds).attributes
: Used forMutationObserver
. Set totrue
to watch for changes to the value of attributes on the node or nodes being monitored.attributeFilter
: Used forMutationObserver
. An array of specific attribute names to be monitored.attributeOldValue
: Used forMutationObserver
. Set totrue
to record the previous value of any attribute that changes when monitoring the node or nodes for attribute changes.characterData
: Used forMutationObserver
. Set totrue
to monitor the specified target node (and, ifsubtree
istrue
, its descendants) for changes to the character data contained within the node or nodes.characterDataOldValue
: Used forMutationObserver
. Set totrue
to record the previous value of a node's text whenever the text changes on nodes being monitored.childList
: Used forMutationObserver
. Set totrue
to monitor the target node (and, ifsubtree
istrue
, its descendants) for the addition of new child nodes or removal of existing child nodes.subtree
: Used forMutationObserver
. Set totrue
to extend monitoring to the entire subtree of nodes rooted attarget
. All of the other properties are then extended to all of the nodes in the subtree instead of applying solely to thetarget
node.box
: Used forResizeObserver
.
Component refs
Creating a ref on an element is a way to facilitate accessing that element in your logic. Making an element a ref is straightforward, the attributre structure is as follows: "data--"+templateName+"."+refName
. For example data--counter.button
for a ref named "button"
on the "counter
component.
Templates
Templates or component templates are what the manager will use to mount and unmount components in the DOM. They contain all the component's logic, state, event listeners, etc...Component templates are registered (enabled) by using the ddd(...)
function and unregistered (disabled) by using the ddd.unregister(...)
function. Effectively, a template is an object describing its component with the following properties: name
, props
, state
, element
, structure
, refs
, handlers
and its lifecycle methods.
Name
Templates must define an unique kebab-cased name to recognize its components. Components with a matching name to a registered template will automatically be mounted. See components.
ddd({
name: "carousel",
})
<div data--carousel></div>
Props
A component instance can be given its own props as seen in components. In the template you can provide a schema to validate the given props.
ddd({
name: "carousel",
props: ddd.object({
loop: ddd.boolean(),
autoplay: ddd.boolean({ optional: true }),
}),
})
<div data--carousel='{ "loop": true }'></div>
State
The initial state for your components. Unlike frameworks like React, data-double-dash
is not reactive, state changes will not automatically update the DOM.
The initial state can either be an object or a function taking the component as a parameter and returning an object.
Element
To validate the type of a component's element you can specify its constructor in the element
property.
ddd({
name: "carousel",
element: HTMLDivElement,
})
Structure
If you want a component's element to have a specific HTML structure you can provide an element schema. This schema is the same as the one used in ddd.element.validate(...)
.
The structure schema can either be given directly or as a function taking the resolved props as a parameter and returning it.
ddd({
name: "swiper",
props: ddd.object({
hasNavigation: ddd.boolean({ optional: true }),
hasPagination: ddd.boolean({ optional: true }),
})
structure: ({ hasNavigation, hasPagination }) => {
const structure = [
{ query: ".swiper-wrapper", min: 1 }
]
if(hasNavigation) {
structure.push(
{ query: ".swiper-button-prev", min: 1, matches: "button" },
{ query: ".swiper-button-next", min: 1, matches: "button" },
)
}
if(hasPagination) {
structure.push({ query: ".swiper-pagination", min: 1 })
}
return structure
},
})
Refs
Refs are a way to quickly access key elements. In the template they are represented as strings. They must have the following format: refName
, refName+modifier
, selector+"--"+refName
or selector+"--"+refName+modifier
.
selector
is a CSS selector like the ones you would find in the querySelector
method, it will be used to match the ref.
modifier
can be either "?"
which makes the ref optional or "[]"
which makes the ref an arrays of elements (use the same ref name multiple times in the HTML).
refName
is a kebab-cased string corresponding to the name of your ref which will be found in the HTML, see refs.
Attributed refs are cached, in practice this means that when deleting the ref element from the DOM, the ref will delete itself asynchronously at the end of the current tick.
You can also create another kind of ref which don't need to have HTML attributes. This kind of ref is a bit like preparing a querySelector
. You can declare these kind of refs (unattributed refs) by following this format: selector+"~~"+refName
or selector+"~~"+refName+modifier
. In this case the selector
will not be used to match but to find the element(s). This kind of refs are not cached, everytime you get them a querySelector
happens.
ddd({
name: "slideshow",
refs: [
"img--images[]",
"ul--list",
"button--next?",
"button--prev?",
"li~~items[]",
],
})
Handlers
Handlers are methods which handle all the events which this component listens to. To define events, see component and events.
A handler can simply be defined as a function or as an object. Defining the handler as an object enables you to add validation for the event
type, for the event.target
type, for the event params and for a CustomEvent
detail
property.
ddd({
name: "slideshow",
state: { currentSlide: 0 },
handlers: {
changeSlide: {
params: ddd.object({
index: ddd.number(),
}),
handler(event, component, params) {
component.state.currentSlide = params.index
// ...
},
},
},
})
<div data--slideshow>
<button
data--slideshow--click='{ "handler": "changeSlide", "params": { "index": 1 } }'
>
slide 1
</button>
<!-- ... -->
</div>
Lifecycle methods
Lifecycle methods get called at different stages of a component's lifetime :
Template Method | Lifecycle | When |
---|---|---|
register() | Template registration | Synchronously after ddd(...) , when the template is successfully registered. |
mount(component) | Component mounting | Asynchronously after ddd(...) (synchronously after ddd.sync() ), when the component is successfully mounted. |
unregister() | Template unregistration | Synchronously after ddd.unregister(...) , when the template is successfully unregistered. |
unmount(component) | Component unmounting | Asynchronously after ddd.unregister(...) (synchronously after ddd.sync() ), when the component is successfully unmounted. |
ddd({
name: "foo",
register() {
// ...
},
mount(component) {
// ...
},
unmount(component) {
// ...
},
unregister() {
// ...
},
})
Reference
ddd
function (template: Template): void
ddd.get
function (): Manager
ddd.get.components
function (element: Element, returnRoot = true): Map<string, Component>
ddd.get.component
function (element: Element, template: string, returnRoot = true): Component | null
ddd.element
function (
html: string,
options?: {
// allows you to replace substrings in `html` before converting it to a node
// this is useful for placeholders such as <div id="__ID__">...</div>
replace?: Record<string, string>;
// defaults to true, removes everything that can execute code from the HTML
sanitize?: boolean;
}
): DocumentFragment
ddd.element.validate
function (
element: Element,
schema: ElementSchema<U>,
): asserts element is GetElementFromHtmlTag<U>
ddd.validate
function (data: unknown, schema: Schema): asserts data is ResolvedSchema<Schema>
ddd.string
// creates a schema to validate strings
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: string) => boolean // additional custom validation function
}
): StringSchema
ddd.number
// creates a schema to validate numbers
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: number) => boolean // additional custom validation function
}
): NumberSchema
ddd.boolean
// creates a schema to validate booleans
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: boolean) => boolean // additional custom validation function
}
): BooleanSchema
ddd.undefined
// creates a schema to validate undefined values
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: undefined) => boolean // additional custom validation function
}
): UndefinedSchema
ddd.null
// creates a schema to validate null values
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: null) => boolean // additional custom validation function
}
): NullSchema
ddd.nullish
// creates a schema to validate nullish values
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: null | undefined) => boolean // additional custom validation function
}
): NullishSchema
ddd.function
// creates a schema to validate functions
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: (...args: any[]) => unknown) => boolean // additional custom validation function
}
): FunctionSchema
ddd.instance
// creates a schema to validate instances of a constructor
function (
constructor: new (...args: any[]) => Object // for example `File` or `Element`
options?: {
optional: true // makes this schema optional in an object
predicate: (data: CorrespondingInstance) => boolean // additional custom validation function
}
): InstanceSchema
ddd.object
// creates a schema to validate object
function (
object: { [property: string]: Schema }
options?: {
optional: true // makes this schema optional in an object
predicate: (data: CorrespondingObject) => boolean // additional custom validation function
}
): ObjectSchema
ddd.array
// creates a schema to validate arrays
function (
types: Schema | Schema[] // allowed types in the array
options?: {
optional: true // makes this schema optional in an object
predicate: (data: CorrespondingArray) => boolean // additional custom validation function
}
): ArraySchema
ddd.union
// creates a schema to validate union (an union is when there's more than a single valid type)
function (
types: Schema | Schema[] // allowed types
options?: {
optional: true // makes this schema optional in an object
predicate: (data: CorrespondingUnion) => boolean // additional custom validation function
}
): UnionSchema
ddd.record
// creates a schema to validate records (records are objects with arbitrary keys)
function (
types: Schema | Schema[] // allowed types in the record values
options?: {
optional: true // makes this schema optional in an object
predicate: (data: CorrespondingRecord) => boolean // additional custom validation function
}
): ArraySchema
ddd.set
// creates a schema to validate sets
function (
types: Schema | Schema[] // allowed types in the set
options?: {
optional: true // makes this schema optional in an object
predicate: (data: CorrespondingSet) => boolean // additional custom validation function
}
): SetSchema
ddd.map
// creates a schema to validate maps
function (
keyTypes: Schema | Schema[], // allowed types for keys in the map
valueTypes: Schema | Schema[] // allowed types for values in the map
options?: {
optional: true // makes this schema optional in an object
predicate: (data: CorrespondingSet) => boolean // additional custom validation function
}
): MapSchema
ddd.any
// creates a schema to accept any value
function (
options?: {
optional: true // makes this schema optional in an object
predicate: (data: unknown) => boolean // additional custom validation function
}
): AnySchema
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
6 months ago
6 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago