1.21.3 • Published 4 months ago

data-double-dash v1.21.3

Weekly downloads
-
License
MIT
Repository
-
Last release
4 months ago

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 be data--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 and remove, 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 make element 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 using setTimeout, 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 using setInterval, 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 the custom event's type that this event will dispatch.
  • detail: An object which will be passed along with the dispatched custom event.
  • cancelable: Whether the disaptched custom event can be canceled, and therefore prevented as if the event never happened.
  • composed: Whether or not the disaptched custom 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 any EventTarget 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 for IntersectionObserver. A string selector (used with closest) which identifies the Element whose bounds are treated as the bounding box of the viewport for the element which is the observer's target.
  • rootMargin: Used for IntersectionObserver. A string with syntax similar to that of the CSS margin property. Each side of the rectangle represented by rootMargin is added to the corresponding side in the root element's bounding box before the intersection test is performed.
  • threshold: Used for IntersectionObserver. A number or an array of numbers representing percentages of the target element which are visible (intersection thresholds).
  • attributes: Used for MutationObserver. Set to true to watch for changes to the value of attributes on the node or nodes being monitored.
  • attributeFilter: Used for MutationObserver. An array of specific attribute names to be monitored.
  • attributeOldValue: Used for MutationObserver. Set to true to record the previous value of any attribute that changes when monitoring the node or nodes for attribute changes.
  • characterData: Used for MutationObserver. Set to true to monitor the specified target node (and, if subtree is true, its descendants) for changes to the character data contained within the node or nodes.
  • characterDataOldValue: Used for MutationObserver. Set to true to record the previous value of a node's text whenever the text changes on nodes being monitored.
  • childList: Used for MutationObserver. Set to true to monitor the target node (and, if subtree is true, its descendants) for the addition of new child nodes or removal of existing child nodes.
  • subtree: Used for MutationObserver. Set to true to extend monitoring to the entire subtree of nodes rooted at target. All of the other properties are then extended to all of the nodes in the subtree instead of applying solely to the target node.
  • box: Used for ResizeObserver.

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 MethodLifecycleWhen
register()Template registrationSynchronously after ddd(...), when the template is successfully registered.
mount(component)Component mountingAsynchronously after ddd(...) (synchronously after ddd.sync()), when the component is successfully mounted.
unregister()Template unregistrationSynchronously after ddd.unregister(...), when the template is successfully unregistered.
unmount(component)Component unmountingAsynchronously 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
1.21.0

4 months ago

1.21.1

4 months ago

1.21.2

4 months ago

1.21.3

4 months ago

1.20.1

4 months ago

1.20.0

4 months ago

1.19.4

5 months ago

1.19.0

5 months ago

1.19.3

5 months ago

1.19.2

5 months ago

1.19.1

5 months ago

1.18.1

5 months ago

1.18.0

5 months ago

1.18.4

5 months ago

1.18.3

5 months ago

1.18.2

5 months ago

1.17.2

5 months ago

1.17.1

5 months ago

1.17.0

5 months ago

1.17.4

5 months ago

1.17.3

5 months ago

1.10.9

6 months ago

1.10.8

6 months ago

1.10.7

6 months ago

1.10.6

6 months ago

1.15.0

6 months ago

1.11.4

6 months ago

1.11.3

6 months ago

1.11.2

6 months ago

1.11.1

6 months ago

1.11.8

6 months ago

1.11.7

6 months ago

1.15.2

6 months ago

1.11.6

6 months ago

1.15.1

6 months ago

1.11.5

6 months ago

1.11.9

6 months ago

1.12.1

6 months ago

1.12.0

6 months ago

1.16.3

6 months ago

1.16.2

6 months ago

1.16.1

6 months ago

1.16.0

6 months ago

1.16.5

6 months ago

1.16.4

6 months ago

1.11.0

6 months ago

1.13.0

6 months ago

1.10.15

6 months ago

1.10.13

6 months ago

1.10.14

6 months ago

1.10.11

6 months ago

1.10.12

6 months ago

1.10.10

6 months ago

1.10.5

6 months ago

1.10.4

6 months ago

1.10.3

6 months ago

1.10.2

6 months ago

1.8.0

6 months ago

1.6.0

6 months ago

1.9.5

6 months ago

1.9.3

6 months ago

1.9.2

6 months ago

1.9.1

6 months ago

1.5.5

6 months ago

1.9.0

6 months ago

1.5.4

6 months ago

1.5.3

6 months ago

1.7.0

6 months ago

1.5.2

6 months ago

1.5.1

6 months ago

1.5.0

6 months ago

1.10.1

6 months ago

1.10.0

6 months ago

1.2.0

6 months ago

1.0.1

6 months ago

1.0.0

6 months ago

1.4.2

6 months ago

1.4.1

6 months ago

1.2.3

6 months ago

1.2.2

6 months ago

1.2.1

6 months ago

0.9.34

6 months ago

1.1.0

6 months ago

0.9.35

6 months ago

0.9.36

6 months ago

0.9.37

6 months ago

0.9.33

6 months ago

0.9.38

6 months ago

0.9.39

6 months ago

1.3.0

6 months ago

0.9.12

7 months ago

0.9.13

7 months ago

0.9.14

7 months ago

0.9.15

7 months ago

0.9.30

7 months ago

0.9.31

6 months ago

0.9.32

6 months ago

0.9.10

7 months ago

0.9.11

7 months ago

0.9.16

7 months ago

0.9.17

7 months ago

0.9.18

7 months ago

0.9.19

7 months ago

0.9.23

7 months ago

0.9.24

7 months ago

0.9.25

7 months ago

0.9.26

7 months ago

0.9.20

7 months ago

0.9.21

7 months ago

0.9.22

7 months ago

0.9.27

7 months ago

0.9.28

7 months ago

0.9.29

7 months ago

0.9.9

7 months ago

0.9.8

7 months ago

0.9.7

7 months ago

0.9.6

7 months ago

0.9.5

7 months ago

0.9.4

7 months ago

0.9.3

7 months ago

0.9.2

7 months ago

0.9.1

7 months ago

0.9.0

7 months ago