npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nartallax/cardboard-dom

v2.0.8

Published

DOM utils for Cardboard

Downloads

15

Readme

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)
	}
})