A very convenient way to generate HTML elements using JavaScript
npm install @nejs/htmlCreating 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.
* Installing
* Getting Started
* API
* Proxy Notes
* Command Symbols
* Composite Elements
* ShadowDOM
* JSDelivr
* Change Log
Install to your npm project using
``sh`
npm i @nejs/html
Or if you want to directly use it on a webpage, see the jsdelivr
section below.
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
`js`
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 HTMLcommands
and exported.
`html`
If you would prefer to create and append the script tag using JavaScript, here
is a nice little pasteable script.
`js
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();
`
In its base form, the syntax looks a bit like
this:
`js`
const element = HTML.create('div', 'text node content');
This is far more terse than the equivalent
`js`
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:
`html
Creating this with JavaScript would be something like the following:
`js
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:`js
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.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' }attributes - an object of additional tag attributes thatelement.setAttribute. So an object{ id: 'identifier' } becomes webComponentName - this must be supplied if the element{ is: value }document.createElement()useDocument - a way to specify an alternate documentdocument. Can be used to denotedocument or another window's documentchildren - an array of HTMLElement object instances thashadow - if preset, this should be either an object orHTMLElement instances that are inserted into the attachoptions and children. Omitting{ mode: 'open',
clonable: true, slotAssignment: 'named' }. These can{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, shadowAdditionally, 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
.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 Some Examples:
`js
// Create a simple element with content:
const element = HTML.create('div', {content: 'Hello, world!'})
document.body.appendChild(element);
``js
// Using the prototype chain proxy, simpler syntax can be
// achieved.
const element = HTML.div('Hello world');
// Hello world
// 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' }),
])
//
//
//
//
`HTML[commands.register]
_↩︎ Back to_ ◌ _ Top_ ◌ _API_
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.`js
HTMLcommands.register;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_ ◌ _ Top_ ◌ _API_
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:`js
[[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 configsEffectively, when a composite element is created, this data is used like
this:
`js
(...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.`js
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.$3
_↩︎ Back to top_In some of these examples you'll see an import like
`js
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.
`js
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;
`js
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
`js
HTMLcommands.register;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.`js
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._
`js
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
`js
'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.`js
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:`html
#shadow-root (open)
`This brings up an important note; the default options passed to
element.attachShadow are:`js
{
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.`js
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