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

wirejs-dom

v1.0.25

Published

Ultralite web component and DOM manipulation library.

Downloads

336

Readme

A bare bones component style JavaScript framework.

Install it

yarn add wirejs-dom

Or

npm install wirejs-dom

Use it

~~Detailed documentation is pending. Until then, here's the gist of it:~~

I am no longer working on detailed documentation for the V1 experience. I am instead working aggressively on the V2 experience. This is sort of what V1 looks like.

const { DomClass } = require('wirejs-dom');

const template = `<myns:post>
	<h3 data-id='title'></h3>
	<p data-id='body'></p> 
	<p data-id='comments'></p>
</myns:post>`;

DomClass(template, () => {
	const self = this;
	MyAPI.fetchComments(this.id).then(comments => {
		// assuming we've also defined a Comment DomClass ...
		self.comments = comments.map(c => new Comment(c));
	});
});

But, more importantly, buckle up for V2. It's going to be neat.


V2 Preview

The new typed API focuses on tagged template literals. This is the inverse of how most front-end frameworks operate with their "hook" (or signal) mechanisms. With these frameworks, you to create state outside the DOM with a "hook" or a signal and inject it into the DOM via JSX. Updates to the state trigger a re-render of the component.

wirejs works primarily in the opposite direction. You write streamlined HTML that emits direct accessors to its state. You can then read or write to the accessor. The DOM node or properties you write to are then updated directly.

There is no component-level re-render unless you re-render it.

The Basics

Import the stuff.

// While this API is still in preview, import from /v2.
import { html, id, text, attribute, list } from 'wirejs-dom/v2';

Use the html tag to create an HTMLElement.

const note = html`<div>
	<b>IMPORTANT!</b> Markup in an "html string" must have a single "container" node.
	It's a DIV in this case. The container DIV is what is returned.
	<br /><br />
	But, you can use whatever tag you like. You could even make one up.
</div>`;

Add the resulting HTMLElement to the DOM or interpolate it in another html tag.

document.body.appendChild(html`
	<div>
		<h1>A Title</h1>
		${note}
		<div>the rest of the app...</div>
	</div>
`);

You can get pretty far with this little building block alone. But, wirejs is a little more dynamic and magical than that.

Accessors and the data property

When you create an html element with wirejs, it also builds up a typed data property from the elements you create with wirjes "accessors". Those accessors are:

  • text(key: string, value?: string)Create a Text node.
  • id(key: string)The contextual HTMLElement.
  • attribute(key: string, value?: string)The contextual Attr.
  • list<T>(key: string, value?: T[], map?: ((item: T) => HTMLElement))A mapped list. Default map creates div elements.
  • list<T>(key: string, map?: ((item: T) => HTMLElement), value?: T[])A mapped list, alternative parameter ordering.

Each accessor receives a key parameter. The key parameter shows up in the data property of the HTMLElement created by your html tag.

Let's see how they work in more detail.

The text() Accessor

This creates a Text node and adds it to the data property of your element.

const element = html`<div>
	Hello ${text('name', 'world')}.
</div>`;

Initially, this will produce the following an element with the following markup:

<div>
	Hello, world.
</div>

The interpolated text('name', 'world') call allows you to both read and write the name property like this:

// This logs "world"
console.log(element.data.name);

// This updates the HTMLElement directly
element.data.name = "Jon";

After setting the name property, the element is immediately updated and its HTML will look like this:

<div>
	Hello, Jon.
</div>

The id() Accessor

The id() accessor exposes data properties to access or replace DOM elements directly.

const element = html`<div>
	<h3 ${id('title')}>some title</h3>
	<p>some content</p>
</div>`;

This produces an element with the following markup:

<div>
	<h3>some title</h3>
	<p>some content</p>
</div>

The element.data.title property refers to the H3 element itself. If you replace it with with another HTMLElement (or Text node) the element is dropped directly into the DOM in its place.

element.data.title = html`<h4>A less important title ... </h4>`;

The updated markup would look like this:

<div>
	<h4>A less important title ... </h4>
	<p>some content</p>
</div>

And after the swap, the new H4 element is now accessible and overwritable via the title property. Because the accessor also refers to the element itself, you can also directly manage its native properties — to add events, for example.

element.data.title.onclick = () => alert('You clicked the title!');

The markup itself will not be updated to reflect this. But, the DOM node will now have the onclick event registered. Clicking the H4 will now result in an alert.

The attribute() Accessor

The attribute() accessor provides a shortcut to expose individual element attributes.

const element = html`<div>
	<h3 ${id('title')}>some title</h3>
	<img src=${attribute('logo', 'optional/default.png')} />
</div>`;

This produces the following markup:

<div>
	<h3>some title</h3>
	<img src='optional/default.png' />
</div>

If you write to the logo property, it will update the DOM accordingly.

element.data.logo = 'special-logo.png';

The updated markup will look like this:

<div>
	<h3>some title</h3>
	<img src='special-logo.png' />
</div>

The list() Accessor

The list() accessor provides access to a list of items with optional mapping between writing and rendering in the DOM. You can do anything you can normally do to an Array to an exposed list accessor and the changes will be remapped as-needed and re-inserted into the DOM.

const element = html`<div>
	People I know.
	${list('names', ['Janet', 'Joseph', 'Jimmy John'])}
</div>`;

By default, list() renders items inside <div> elements. So, the resulting markup will be similar to this (spacing added here for readability):

<div>
	People I know.
	<div>Janet</div>
	<div>Joseph</div>
	<div>Jimmy John</div>
</div>

I can update individual list items using the names property:

element.data.names[1] = 'Joseph (Who goes by Joe)';

The HTMLElement associated with the 2nd item of the list will be updated directly, and the markup will look like this:

<div>
	People I know.
	<div>Janet</div>
	<div>Joseph (Who goes by Joe)</div>
	<div>Jimmy John</div>
</div>

I could sort() and reverse() the list:

element.data.names.sort().reverse();

The updated markup:

<div>
	People I know.
	<div>Joseph (Who goes by Joe)</div>
	<div>Jimmy John</div>
	<div>Janet</div>
</div>

(You can also push(), pop(), shift(), unshift(), slice(), and splice().)

You can also replace a list wholesale.

element.data.names = ['Tony Stark', 'Bruce Banner', 'Stephen Strange'];

The updated markup:

<div>
	People I know.
	<div>Tony Stark</div>
	<div>Bruce Banner</div>
	<div>Stephen Strange</div>
</div>

If you don't like div's, you can map your data however you like:

const element = html`<div>
	People I know.
	<ol>${list('names',
		['Tony Stark', 'Bruce Banner', 'Stephen Strange'],
		(name: string) => html`<li>${name}</li>`
	)}</ol>
</div>`;

This will produce markup like this:

<div>
	People I know.
	<ol>
		<li>Tony Stark</li>
		<li>Bruce Banner</li>
		<li>Stephen Strange</li>
	</ol>
</div>

When you read the accessor, it will still look like a list of strings.

You can also omit the initial list data to be set later.

const element = html`<div>
	People I know.
	<ol>${list('names', (name: string) => html`<li>${name}</li>`)}</ol>
</div>`;

Your initial markup will look like this:

<div>
	People I know.
	<ol><!-- Your items will show up here when you write them. --></ol>
</div>

Note

1. Setting a list() from an Array creates a copy of the original. Updates to the original Array do not update the DOM.

const names = ['Tony Stark', 'Bruce Banner', 'Stephen Strange'];
element.data.names = names;

// This doesn't do anything to `element.data.names`.
names.push('The Other Steve');

2. list() uses an empty Text node to keep its place. This is true regardless of whether there are items in the list. The empty node just tracks where the list starts and makes it simpler and faster for wirejs to rebuild the list in the DOM as necessary.

<div>
	People I know.
	<ol><!-- An empty Text node lives here. (Don't remove it!) --></ol>
</div>

The node() Accessor

If all else fails, or if you just need to embed a little logic, you can use the node() accessor, which allow you to create a named node of any kind using a mapper function and optionally a default value.

const app = html`<div>
	The number ${node('number', 2,
		(n: number) => n % 2 === 0 ?
			html`<i>${n} is even</i>` : 
			html`<b>${n} is odd</i>`
		)}.
</div>`;

// sets `data` to 5 and invokes the mapper to regenerate that portion of the element.
app.data.number = 5;

Event Handlers

You have two options for event handlers.

  1. The attribute() accessor.Good for one-off event handlers you may need to overwrite later.
  2. The id() accessor.Good for setting many event handlers, either based on a template or in cases where you need to swap them out later.
  3. Inline handlers.Good for all the other times.

Here's what an inline handler might look like:

const element = html`<div>
	Hello, ${text('name', 'world')}.
	( <button onclick=${() => element.data.name = 'Jon'}>Rename</button> )
</div>`;

The first parameter for inline handlers is the Event that triggered them, just like a normal event handler. Closures work as expected, allowing you to write some pretty self-contained components.

const app = html`<div>
	<h2>My Todo App</h2>
	<ol>${list('todos', (text) => html`<li>${text}</li>`)}</ol>
	<form onsubmit=${event => {
		event.preventDefault();
		app.data.todos.push(app.data.newTodo);
		app.data.newTodo = '';
	}}>
		<input type='text' value=${attribute('newTodo')} />
		<button type='submit'>Add</button>
	</form>
</div>`;
document.body.appendChild(app);

This is not alawys recommended. But, it's great for smaller, inlined, or otherwise self-contained components. It can often save on some of the maintenance overhead of creating a bigger or more complex component.

document Events

You can monitor when an html element is added to or removed from the document like this:

const app = html`<div>My div</div>`
	.onadd(() => console.log('added', app))
	.onremove(() => console.log('removed', app))
;

These methods also receive the element itself as a parameter for convenience when defining these handlers inline without an explicit variable to refer to.

const staticGreeting = (name: string) => html`<div>hello, ${name}.</div>`
	.onadd(self => console.log(`greeting for ${name} added`, self))
	.onremove(self => console.log(`greeting for ${name} removed`, self));

Adding method and properties

You can extend elements inline with the extend() method.

const app = html`<div>Hello, ${text('name', '___')}.</div>`
	.extend(() => ({
		setName(name: string) {
			app.data.name = name;
		}
	}));

document.body.append(app);
app.setName('World');

And again, if you don't have a variable to refer to because you're building everything inline, extend() receives the element itself as a parameter.

const buildApp = () => html`<div>Hello, ${text('name', '___')}.</div>`
	.extend((self) => ({
		setName(name: string) {
			self.data.name = name;
		}
	}));

const app = buildApp();
document.body.append(app);
app.setName('World');

And finally, if you prefer not to pollute the element itself, you can nest your extensions any way you like:

const buildApp = () => html`<div>Hello, ${text('name', '___')}.</div>`
	.extend((self) => ({
		// these get merged into the existing `data` property
		data: {
			setName(name: string) {
				self.data.name = name;
			}
		},

		// these get tacked onto a new extensions property
		debug: {
			log() {
				console.log('something about', self);
			}
		}
	}));

High Level Backlog

(In no particular order.)

  1. A good CSS / Styling mechanism
  2. Allow text, attribute, and list to be defined externally and injected N times
  3. A good rehydration mechanism
  4. SSR+SSG with rehydration support
  5. Allow signal incorporation into html, text, attribute, and list tags?

This is not a comprehensive list of things I'm aiming at — just want to be clear that these things are pretty high on my list!