Skip to content

nartallax/cardboard-dom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

65 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cardboard DOM

A way to link Cardboard boxes to DOM.

Install

npm install @nartallax/cardboard
npm install @nartallax/cardboard-dom

Note that @nartallax/cardboard is a peer dependency and must be installed first.

Usage: rendering DOM elements

First function you need to know of is tag.
tag creates an HTML element with parameters you provide:

import {tag, initializeCardboardDom} from "@nartallax/cardboard-dom"

// this function MUST be called before any DOM manipulation
// (in later examples this call is omitted)
await initializeCardboardDom()

const root = tag({class: "root-container"}, [
	tag({tag: "h1"}, ["Hello world!"]),
	"Lorem ipsum or smth",
	tag({
		tag: "button",
		attrs: {value: "Click me!"},
		onClick: () => console.log("clicked!")
	})
])

// after that, you may do whatever you want with the element
document.body.append(root)

Using boxes: general idea

Now to the main feature of this library - how to subscribe to a box without making memory leak.
This library allows you to bind box to an DOM node. When the node is inserted into DOM - library will subscribe to the box; when the node is removed - library will unsubscribe. This way a box is never subscribed to unless it can actually do something with DOM.
For this exact purpose bindBox function exists:

import {tag, bindBox} from "@nartallax/cardboard-dom"
import {box} from "@nartallax/cardboard"

let nameBox = box("Alice")
let nameEl = tag()

let handler = (name: string) => nameEl.setAttribute("data-name", name)
// this binds the nameBox to nameEl.
// callback will be executed immediately, and also each time the box is inserted into DOM.
bindBox(nameEl, nameBox, handler)

// you can unbind box later if you don't need it anymore, but that's optional
// (nothing bad will happen if you won't do it)
// note that this will unbind all the boxes that use that handler
unbindBox(nameEl, handler)

Subscribing to boxes directly (like, with .subscribe() method) is still an option, but you will almost never really need it. Direct call of .subscribe() should be avoided, as it is the way to create an accidental memory leak.

Using boxes: shortcuts

But that's a bit tedious, to bind each individual box.
Fortunately, tag allows you to pass boxes in place of most values:

import {tag} from "@nartallax/cardboard-dom"
import {box} from "@nartallax/cardboard"

let nameBox = box("Bob")
let colorBox = box("#00f")
let classNameBox = box("name-label")
let isHighlightedBox = box(false)
let nameEl = tag({
	// you can pass a box as attribute value
	attrs: {"data-name": nameBox},
	// you can pass box as style value
	style: {backgroundColor: colorBox},
	// you can also pass boxes as parts of class name
	class: ["some-label", classNameBox, {highlighted: isHighlightedBox}]
	// ... and other stuff
}, 
// you can also pass box as a child; a text node with the content of the box will be created
// ...but you cannot pass box with a DOM node this way
["The name is: ", nameBox])

Passing box in place of value is equivalent to passing just value, and then binding a box to that element and updating value in the callback. So it's pretty intuitive - if you pass a box, tag will handle binding for you.

As it says above, you cannot pass a box containing an HTMLElement as child to tag.
This is a design choice; boxes exist to store data, and HTMLElement is a product of processing that boxed data into something that you can show to the user. It is extension of original Cardboard rule "don't put box into box".

Container tags: arrays

Working with arrays is hard.
Fortunately, this library has a solution for that - container tags:

import {tag, containerTag} from "@nartallax/cardboard-dom"
import {box} from "@nartallax/cardboard"

let students = box([
	{name: "Alice", id: 1}, 
	{name: "Bob", id: 5}, 
	{name: "Cody", id: 2}
])
let el = containerTag(
	// tag definition, just like with `tag` (that's optional)
	{class: "student-container"},
	// a box with array of something
	students,
	// `getKey` callback. you should return stable keys from this function
	student => student.id,
	// `render` callback. this callback receives box with an element of the array,
	// and supposed to return an HTMLElement
	studentBox => tag({
		class: "student-line", 
		attrs: {"data-student-id": studentBox.prop("id")}
	}, ["Student: ", studentBox.prop("name")])
)

// and then you have your container tag
// it will update its own children when original array box changes its value
document.body.append(el)

Container tags: individual boxes

There's another use of containerTag function, and that's a way to transform a box (or several) into DOM nodes:

import {tag, containerTag} from "@nartallax/cardboard-dom"
import {box} from "@nartallax/cardboard"

let nameBox = box("Dan")
let ageBox = box(143)

// you can also have description, with class names and such,
// and only one box instead of array if you want
let el = containerTag([nameBox, ageBox], (name, age) => tag([`This is ${name}, aged ${age}`]))

Note that this is rare case. Usually you want to do that without container tags, because each change of any of the box values will lead to creation of fresh DOM nodes.
It's inavoidable sometimes; for example, if you have a router - it's perfectly reasonable to render new page from scratch when page address changes.

Controls

Controls (also called components in other UI frameworks) are a way to organise your code.
They are totally optional, you can just render everything in your main() function, but that's not very nice, is it?

Basic way to define a control is to just make a function. Usually first argument of that function is an object with properties, and second argument is array of children, if you expect them to be passed (but that's not enforced):

import {tag} from "@nartallax/cardboard-dom"
import {MRBox} from "@nartallax/cardboard"

interface ButtonProps {
	label?: MRBox<string>
	onClick?: () => void
}

export const Button = (props: ButtonProps, otherChildren?: HTMLElement[]) => {
	return tag({
		tag: "button",
		class: "my-custom-button"
		onClick: props.onClick
	}, [
		props.label,
		...(otherChildren ?? [])
	])
}

let myButton = Button({label: "uwu", onClick: () => console.log("uwu!")})

In example above, all properties are optional (they shouldn't be in real life, but for sake of example they are). So, naturally, sometimes you may want to not pass them at all. But that's not something you could do with just a function; you will need to define overrides, then resolve arguments in actual function... that's tedious.
Fortunately, there's a function that will do just that for you: defineControl:

export const Button = defineControl((props: ButtonProps, otherChildren) => {
	// otherChildren here are not merely `HTMLElement[]`, but `HTMLChildArray`
	// this allows user to pass strings, boxes of strings, nulls, undefineds, numbers...
	// you are expected to pass children to `tag` anyway, and make it handle everything

	/* All the same here as in example above */
})

let myButton = Button() // look, no props!

Using boxes: other DOM values

There are other values in DOM that are not (always) bound to elements you can render: page location, local storage, CSS variables.
To interface with them, an overload of bindBox exists:

import {bindBox} from "@nartallax/cardboard-dom"
import {box} from "@nartallax/cardboard"

let pathBox = box("")

bindBox(
	// as ususal, any box requires an element to be bound to:
	document.body,
	// the box we're binding
	pathBox, 
	// description of what to bind this box to
	{
		// there are other types, make sure to check out the typings
		type: "url", 
		// we only need path, so we ask for path:
		path: true
	}
)

console.log(pathBox.get()) // it's "/" if you're in the root of the page

// you can also set it, and changes will be propagated to location:
pathBox.set("/path/to/some/page")
console.log(window.location + "") // http://localhost/path/to/some/page

There is a shortcut that will create a box for you (not only for location binding, for other DOM values too):

import {urlBox} from "@nartallax/cardboard-dom"

const pathBox = urlBox(document.body, {path: true})

Handling DOM insertion/removal

If you need to do something smart with DOM nodes, that will require you to wait for adding/removing node from DOM - you can use onMount function:

import {tag, onMount} from "@nartallax/cardboard-dom"

let el = tag()

onMount(el, () => {
	console.log("Hey, the element was inserted into DOM:", el)
	// returning a function is optional
	return () => {
		console.log("Hey, the element was removed from DOM:", el)
	}
})

About

DOM utils for Cardboard

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published