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

@nejs/html

v3.0.6

Published

A very convenient way to generate HTML elements using JavaScript

Downloads

491

Readme

ne-html

A very convenient way to generate HTML elements using JavaScript

Overview

Creating elements in a pure JavaScript environment can be a troublesome affair. It requires a lot of steps per element and nesting and creating many can quickly become something you don't want to deal with. In environments such as Angular, or React, working with web components can mean additional steps as well.

While React and other frameworks are lighter weight than something like Angular, working with pure JavaScript in the latter space might be desirable in some cases. An example is when other frameworks want to manage two way binding and find themselves battling an Angular component that wishes to do the same.

HTML allows creating many HTML elements in JavaScript with little fuss and in a manner that other devs on your team can read legibly.

Additionally, it provides a mechanism that allows you to register a previously created element (within the same runtime) that can be given a name. This composite element can then be parameterized and created in a clean way without having to deal directly with things like webcomponents.

Finally, there is support for attaching a shadowDOM to your created elements and inserting other elements quickly and easily.

Table of Contents

Installing

↩︎ Back to top

Install to your npm project using

npm i @nejs/html

Or if you want to directly use it on a webpage, see the jsdelivr section below.

Using jsdelivr cdn

↩︎ Back to top

JS Delivr makes it very easy to grab a version of the library right from the browser. To fetch the latest ESM module version you can write some code like the following and add it your code

const {
  HTML,
  commands,
} = await import (
  'https://cdn.jsdelivr.net/gh/nyteshade/ne-html/dist/esm/html.js'
);

Or in non ESM environments, the script can be added using a script tag, which will create the var nejs_html which will have the object properties HTML and commands exported.

<script type="application/javascript" src="https://cdn.jsdelivr.net/gh/nyteshade/ne-html/dist/@nejs/html.bundle.latest.js"></script>

If you would prefer to create and append the script tag using JavaScript, here is a nice little pasteable script.

async function nsJsHtmlScript(useDocument) {
  const doc = useDocument ?? top.document;
  const deferred = { promise: undefined };
  const jsDelivrBase = 'https://cdn.jsdelivr.net';
  const url = new URL(
    '/gh/nyteshade/ne-html/dist/@nejs/html.bundle.latest.js',
    jsDelivrBase
  );

  deferred.promise = new Promise((resolve, reject) => {
    Object.assign(deferred, { resolve, reject })
  })

  const scriptTag = document.createElement('script');
  scriptTag.setAttribute('type', 'application/javascript');
  scriptTag.setAttribute('src', url.href);
  scriptTag.onload = function() {
    deferred.resolve(globalThis?.nejs?.html);
  }

  doc.body.append(scriptTag);
  return deferred.promise;
}

const { HTML, commands } = await nsJsHtmlScript();

Getting Started

↩︎ Back to top

In its base form, the syntax looks a bit like this:

const element = HTML.create('div', 'text node content');

This is far more terse than the equivalent

const element = document.createElement('div');
element.append(document.createTextNode('text node content'));

And while this is not a big deal, this is also a very small example, and almost never do you need a contrived example as simple as this. When you need to set attributes and styles, gets even more complex.

Let's look at this MDN example:

<div class="preference">
  <label for="cheese">Do you like cheese?</label>
  <input type="checkbox" name="cheese" id="cheese" />
</div>

<div class="preference">
  <label for="peas">Do you like peas?</label>
  <input type="checkbox" name="peas" id="peas" />
</div>

Creating this with JavaScript would be something like the following:

const preference1 = document.createElement('div');
const label1 = document.createElement('label');
const textInput1 = document.createElement('input');

const preference2 = document.createElement('div');
const label2 = document.createElement('label');
const textInput2 = document.createElement('input');

label1.append(document.createTextNode('Do you like cheese?'));
label1.setAttribute('for', 'cheese');
textInput1.setAttribute('type', 'checkbox');
textInput1.setAttribute('name', 'cheese');
textInput1.setAttribute('id', 'cheese');
preference1.classList.add('preference');
preference1.append(label1, textInput1);

label2.append(document.createTextNode('Do you like peas?'));
label2.setAttribute('for', 'peas');
textInput2.setAttribute('type', 'checkbox');
textInput2.setAttribute('name', 'peas');
textInput2.setAttribute('id', 'peas');
preference2.classList.add('preference');
preference2.append(label2, textInput1);

However, using the HTML class, it looks more like this:

const preference1 = HTML.div({
  class: 'preference',
  children: [
    HTML.label({for: 'cheese', content: 'Do you like cheese?' }),
    HTML.input({type: 'checkbox', id: 'cheese', name: 'cheese' }),
  ]
});

const preference2 = HTML.div({
  class: 'preference',
  children: [
    HTML.label({for: 'peas', content: 'Do you like peas?' }),
    HTML.input({type: 'checkbox', id: 'peas', name: 'peas' }),
  ]
});

API

↩︎ Back to top

The HTML.create() method is the primary interface. As hinted above, you can use the tag name as a property off of HTML, but it is actually a proxied shortcut. It also uses the HTML.create() method.

HTML.create()

Creates an HTML element based on specified options, applying attributes, styles, content, and potentially a shadow DOM with custom CSS variables.

Strings supplied for 'content' will be wrapped in a DOM TextNo rather than applied as innerHTML; this is intentional. The HTML.create() method can receive its input in one of two manners, either in normal ordered input format (as with most functions) or it can be given a name and then supplied an obje with more values. This latter format provides more flexibility and some helper values that translate automatically into their expected locations on the created element.

Ordered Parameters (only name is required)

  1. name - the tag name
  2. content - optional string of content for the tag
  3. style - an object with style properties that will be applied as a style attribute. So { fontName: 'courier' } becomes `
  4. attributes - an object of additional tag attributes that are applied using element.setAttribute. So an object with { id: 'identifier' } becomes <tag id="identifier">
  5. webComponentName - this must be supplied if the element being created is a web component. This should be a string that will automatically be converted to { is: value } before being used with document.createElement()
  6. useDocument - a way to specify an alternate document object than the global document. Can be used to denote a frame's document or another window's document
  7. children - an array of HTMLElement object instances tha or strings that will be appended to the created element.
  8. shadow - if preset, this should be either an object or an array. If supplied as an array, it will represent HTMLElement instances that are inserted into the attach shadowRoot of the created element. If it is an object it should have the keys options and children. Omitting either value will result in defaults being added. The default shadow dom options are { mode: 'open', clonable: true, slotAssignment: 'named' }. These can be overridden individually or as a whole by specifying each and their new value in `{shadow: {options: { ... }}}

Object Parameters (only name is required)

  1. name
  2. config The config can process the above ordered properties when given as object keys. These map, in the same order, as the ordered parameters that were just described above: content, style, attributes, webComponentName, useDocument, children, shadow

Additionally, the following properties can be supplied

  • class: this property becomes a class="value" attribute on the resulting element.
  • classes: this must be an array of strings, non-string values will be ignored. Each will be joined with a space in between and appended to any previously existing class attribute. If no previous class attribute exists, one will be created. If the resulting filter of non-strings leaves an empty string, and not previous class attribute was otherwise specified, no class attribute will be create
  • dataset: any object supplied here will have its keys and values set on the element.dataset object. When assigning keys and values to this object in a browser they become attributes on a tag with the 'data-' prefix. These values must be valid names or an error will occur. Camel cased values will be shown with dashes. So 'greatScott' become <tag data-great-scott>.

Finally, any properties in the config object that have not been mentioned, will be translated to an attribute of the resulting element. So HTML.div({id: 'fun'}) becomes <div id="fun"></div>

Some Examples:

// Create a simple element with content:
const element = HTML.create('div', {content: 'Hello, world!'})
document.body.appendChild(element);
// Using the prototype chain proxy, simpler syntax can be
// achieved.
const element = HTML.div('Hello world');
// <div>Hello world</div>
// This syntax is also overloaded for convenience in many ways
const element = HTML.div([
  HTML.label({ for: 'input-name', content: 'Name' }),
  HTML.input({ id: 'input-name, type: 'text' }),
])
// <div>
//   <label for="input-name">Name</label>
//   <input type="text" id="input-name">
// </div>

HTML[commands.register] ↩︎ Back to TopAPI

Registers a factory function under a given name with optional configuration, binding context, and additional arguments. This method stores the factory function and its associated data in a centralized storage, allowing for retrieval and utilization elsewhere in the application. For read only or otherwise immutable components, the factories can be fairly simply. Simply stamping down a new component as one might expect.

For composite components that support dynamism in their creation a more complex factory is often required, but also engenders greater reusability. See example

  • { string } name - The unique name to register the factory function under.
  • { Function } factoryFunction - The factory function to register.
  • { Object } [config={}] - Optional configuration object for the factory function.
  • { any } thisArg - The value of this to be used when invoking the factory function.
  • { ...any } args - Additional arguments to pass to the factory function upon invocation.
HTML[commands.register](
  'SmRedButton',
  buttonFactory,
  {color: 'red'},
  this,
  'small'
);

const button = HTML.SmRedButton();

// This registers a `buttonFactory` under the name 'SmRedButton'
// with a configuration specifying the color as 'red', binds
// `this` for context, and passes 'small' as an argument.

HTML[commands.registered] ↩︎ Back to TopAPI

Invoking the HTML[commands.registered]() function will return a Iterable that can be walked or converted to an array using either [...HTML[commands.registered]()] or Array.from(HTML[commands.registered]()). The elements inside are a set of entries, in the format:

[[registeredName, registeredMetadata]]

Each registeredMetadata is a Map that has at least these four keys registered for each composite component

  • factory - the function that generates html elements when invoked
  • config - the preset configuration that allows you to, by default, get different output from the same function under differently registered names
  • thisArg - an optional this for the factory function execution. Note: if you supply a big arrow function (() => {}) then you will not be able to apply a thisArg to its execution. Use a normal function if this is a need you have.
  • args - note that arguments here are essential the first n-arguments in order, but if your factory doesn't support working with configs

Effectively, when a composite element is created, this data is used like this:

(...dynamicArgs) => factory.call(
  thisArg,
  config,
  ...args,
  ...dynamicArgs
)

Where args are the arguments at the time of registration, and dynamicArgs are those passed into the call.

const button = HTML.SmRedButton(clickHandler);

// would result in an invocation similar to the following, assuming
// you were using the example used in HTML[commands.registered]
factory.call(thisArg, {color: 'red'}, 'small', clickHandler);

A note on the proxy in the prototype (what?!)

↩︎ Back to top

The HTML class has a proxy inserted in its prototype chain. This proxy allows you to do the magic that is HTML.div instead of the more verbose HTML.create('div').

The way it works is that the proxy checks for previously registered composite elements (see below) and then it checks if the property was 'create' (as in HTML.create). If it was neither of those, then it invokes HTML.create using the property name as the tagName.

Since there is no div property on the HTML class, it considers this property access to evaluate to a bound version of HTML.create with the already supplied first parameter, the tag name, of div.

What are the commands symbols?

↩︎ Back to top

In some of these examples you'll see an import like

import {HTML, commands} from '@nejs/html';

Well as noted above, we don't want to pollute the HTML property space with anything that might be a tag you want to use in your application. The proxy in the prototype, checks for string properties, not symbols.

Ala, extra functionality via the magic of the Symbol class. If you don't know what a symbol is in JavaScript, take a look at the MDN site for Symbol.

The only real command of import for external users is the register command.

  • commands.register - this is used to tell HTML that you have a factory function that when invoked will return a valid HTMLElement. While no type checking is done, failure to follow this pattern may result in confusion.

Composite elements

↩︎ Back to top

Sometimes you may have created a custom element that you want to reuse, again without creating something like an Angular component or installing React. In this case, the HTML class allows you to register a factory function that returns an element.

Let's take the above example with a label and checkbox input type. If you'll recall it looked like this.

const preference1 = HTML.div({
  class: 'preference',
  children: [
    HTML.label({for: 'cheese', content: 'Do you like cheese?' }),
    HTML.input({type: 'checkbox', id: 'cheese', name: 'cheese' }),
  ]
});

Let's turn this into a composite, and reusable element. Factory functions receive a config object as their first parameter. This parameter provides a non-argument based method of customizing the output of the factory function.

Functionally, there are three key bits of information a labeled checkbox needs to track. The id, which needs to match the for attribute of the label.. The name attribute which is important for the form to know which checkbox was checked. And lastly, the text content for the label itself.

If we want to default to "preference" for the class of the containing div, then we don't need to do anything, but we can make it flexible by using that value unless its defined;

const defaultConfig = {
  label: 'undefined',
  name: 'undefined',
  id: 'undefined',
  class: 'preference',
}

function LabeledCheckbox(config = defaultConfig) {
  const id = config?.id ?? Math.random().toString(36).slice(2);
  const name = config?.name;
  const labelText = config?.label;

  const element = HTML.div([
    HTML.label({ for: id, content: labelText }),
    HTML.input({ type: 'checkbox', id, name })
  ]);

  element.classList.add(config?.class ?? 'preference');

  return element;
}

At this point, if we call LabeledCheckbox() we will get the elements but there are going to be a lot "undefined" values that we don't want and its pretty verbose to specify the config each time.

So lets register it as a factory and see what it looks like

HTML[commands.register]('LabeledCheckbox', LabeledCheckbox, {
  label: 'Do you like cheese?',
  name: 'cheese',
});

const checkbox1 = HTML.LabeledCheckbox();

This is pretty neat, but it will always ask the same question and by itself, that's not very helpful. Let's modify our implementation of LabeledCheckbox.

function LabeledCheckbox(config = defaultConfig, ...args) {
  const [argsLabel, argsName] = args;

  const id = config?.id ?? Math.random().toString(36).slice(2);
  const name = config?.name;
  const labelText = argsLabel ?? config?.label ?? 'ERROR';

  const element = HTML.div([
    HTML.label({ for: id, content: labelText }),
    HTML.input({ type: 'checkbox', id, name })
  ]);

  element.classList.add(config?.class ?? 'preference');

  return element;
}

Now if we register this modified version which takes into account the supplied arguments we get something more useful. Ideally, error checking and other measures should go into your factory functions.

const preferences = [
  HTML.LabeledCheckbox('Do you like cheese?', 'cheese'),
  HTML.LabeledCheckbox('Do you like peas?', 'peas'),
];

document.body.append(...preferences);

This is nice, neat, tidy and will feel a lot like using webcomponents, Angular components or React. But it will be plain JavaScript.

A final word about the shadowDOM

↩︎ Back to top

ShadowDOM elements allow you to simplify your usage of shadowRoot and attaching such to your elements after creation. While most of the elements that use a shadowRoot are registered webcomponents, you can add these to supported elements that are not such.

The following elements are allowed to have a shadow root attached

'article', 'aside', 'blockquote', 'body', 'div', 'footer',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'main', 'nav',
'p', 'section', 'span'

Additionally any element that has a tag name with a hyphen in it will be allowed by the browser.

Creating an element with a shadowRoot using HTML is very simple. It looks like this.

const shadyDiv = HTML.div({shadow: [
  HTML.label({ for: 'name', content: 'What is your name?' }),
  HTML.input({ type: 'text', placeholder: 'Your name' }),
]})

Inspecting shadyDiv in a browser will often look like this:

<div>
  #shadow-root (open)
</div>

This brings up an important note; the default options passed to element.attachShadow are:

{
  mode: 'open',
  clonable: true,
  slotAssignment: 'named',
}

These can be overridden by using the full object syntax when adding a shadowRoot to your element using HTML. You will need to supply one of the default applied values with a different value to change it.

const shadyDiv = HTML.div({shadow: {
  options: {
    mode: 'closed',
    clonable: false,
    slotAssignment: 'manual',
    // ...any other attachShadow() config value
  },
  children: [
    HTML.label({ for: 'name', content: 'What is your name?' }),
    HTML.input({ type: 'text', placeholder: 'Your name' }),
  ],
}})

Changelog

↩︎ Back to top

  • 3.0.6 - forgot to run npm build

  • 3.0.5 - some additional protections put in place around cssVar.set, as well as some additional freaking TypeScript ensured values put into place.

  • 3.0.3 - npmjs not responding properly, trying version bump

  • 3.0.2 - More typescript crap

  • 3.0.1 - Le sigh. Small oversight in cssVar property.

  • 3.0.0 - Provides more robust options.content conversion for text nodes. This will allow types to better and more naturally be converted whereas previously it was a bit too blind.

    Additionally full documentation for registered composite components and all typed methods are added to the built html.d.ts file so that TypeScript users, misguided as they are, will see documentation in its full glory.

    Major rev due to changing how levels and composite components are registered. Previously they were set to be invoked as a separate command but I realised that since we were deploying to a browser my previous concerns about horizontal scaling did not apply. Its not a server side component.

  • 2.4.3 - Adding comments to html.d.ts and fixing a small type issue.

  • 2.4.2 - TypeScript, you are the reason we cannot have nice things

  • 2.4.1 - TypeScript, ever annoying, needs some hand crafted types for HTML since it uses a Proxy in its prototype chain

  • 2.4.0 - Added unit tests for HTML.Levels and significantly improved documentation.

  • 2.3.0 - Added in the HTML.Levels composite element both as an example as well as the first use case upon which this library was heavily tested.

  • 2.1.0 - Added basic vitest tests and allowed registered composite elements to have a specified thisArg and variable parameters making them more reusable and configurable.

  • All previous versions - anything prior to 2.1.0 is considered to be alpha and buggy. Only present for historical reasons.