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

@radically-straightforward/javascript

v1.0.0

Published

⚙️ Browser JavaScript in Tagged Templates

Downloads

176

Readme

Radically Straightforward · JavaScript

⚙️ Browser JavaScript in Tagged Templates

Installation

$ npm install --save-dev @radically-straightforward/javascript

Note: We recommend installing @radically-straightforward/javascript as a development dependency because @radically-straightforward/build removes the javascript`___` tagged templates from the server code and bundles the browser JavaScript.

Note: We recommend the es6-string-html Visual Studio Code extension to syntax highlight browser JavaScript in tagged templates.

Usage

import javascript, { JavaScript } from "@radically-straightforward/javascript";

JavaScript

export type JavaScript = string;

A type alias to make your type annotations more specific.

javascript()

export default function javascript(
  templateStrings: TemplateStringsArray,
  ...substitutions: any[]
): JavaScript;

A tagged template for browser JavaScript:

javascript`
  console.log(${["Hello World", 2]});
`;

Note: Browser JavaScript is represented as a string and this tagged template works by performing string interpolation. The substitutions are JSON.stringify()ed. This is conceptually simple and fast. To extract and process the browser JavaScript refer to @radically-straightforward/build.

Browser JavaScript

css`
  @import "@radically-straightforward/javascript/static/index.css";
`;

javascript`
  import * as javascript from "@radically-straightforward/javascript/static/index.mjs";
`;

Importing this module enables the following features:

Live Navigation

Detect that the user is following a link, submitting a form, or navigating in browser history and overwrite the browser’s default behavior: instead of loading an entire new page, morph() the current page into the new one. This speeds up navigation because CSS and browser JavaScript are preserved. Also, it allows for preserving some browser state (for example, scroll position on a sidebar) through careful use of the same key="___" attribute across pages.

Live Navigation enhances <form>s in the following ways:

If the pages include the <meta name="version" content="___" /> meta tag and the versions differ, then Live Navigation is disabled and the user is alerted through a tippy() to reload the page. The tippy() is attached to an element with key="global-error" or the <body>’s first child.

When loading a new page, a progress bar is displayed on an element with key="progress-bar" that is the last child of <body>. This element may be styled via CSS.

An <a> or <form> may opt out of Live Navigation by setting the property element.liveNavigate = false.

Code Execution through the javascript="${javascript`___`}" Attribute

When the page is loaded, the browser JavaScript in the javascript="${javascript`___`}" attribute is executed. This is made to work along with the @radically-straightforward/build package, which extracts browser JavaScript from the server code.

Custom Form Validation

The default browser form validation is limited in many ways:

  • Email verification in <input type="email" /> is too permissive to be practical, allowing, for example, the email address example@localhost, which is technically valid but undesirable.

  • Custom validation is awkward to use.

  • It isn’t possible to control the style of the error messages.

@radically-straightforward/javascript overwrites the default browser behavior and introduces a custom validation mechanism. See validate() for more information.

Warn about Unsaved Changes before Leaving the Page

If the user has filled a form but hasn’t submitted it and they try to leave the page, then @radically-straightforward/javascript warns that they will lose data. See isModified() for more information.

configuration

export const configuration;

Global configuration for browser JavaScript.

Example

javascript.configuration.environment = "development";

liveConnection()

export async function liveConnection(requestId);

Open a Live Connection to the server.

If a connection can’t be established, then an error message is shown in a tippy(). The tippy() is attached to an element with key="global-error" or the <body>’s first child.

If the content of the meta tag <meta name="version" content="___" /> has changed, a Live Connection update doesn’t happen. Instead, a message is shown in a tippy() instructing to reload the page.

If configuration.environment === "development" then the page reloads when the connection is closed and reopened, because presumably the server has been restarted after a code modification.

mount()

export function mount(element, content, event = undefined);

morph() the element container to include content. execute() the browser JavaScript in the element. Protect the element from changing in Live Connection updates.

documentMount()

export function documentMount(content, event = new Event("DOMContentLoaded"));

Note: This is a low-level function used by Live Navigation and Live Connection.

Similar to mount(), but suited for morphing the entire document. For example, it dispatches the event to the window.

If the document and the content have <meta name="version" content="___" /> with different contents, then documentMount() displays an error message in a tippy() and doesn’t mount the new document. The tippy() is attached to an element with key="global-error" or the <body>’s first child.

morph()

export function morph(from, to, event = undefined);

Note: This is a low-level function—in most cases you want to call mount() instead.

Morph the contents of the from container element into the contents of the to container element with minimal DOM manipulation by using a diffing algorithm.

If the to element is a string, then it’s first converted into an element with stringToElement().

Elements may provide a key="___" attribute to help identify them with respect to the diffing algorithm. This is similar to React’s keys, but sibling elements may have the same key (at the risk of potentially getting them mixed up if they’re reordered).

When morph() is called to perform a Live Connection update (that is,event?.detail.liveConnectionUpdateis true), elements may set a liveConnectionUpdate attribute, which controls the behavior of morph() in the following ways:

  • When from.liveConnectionUpdate is false, morph() doesn’t do anything. This is useful for elements which contain browser state that must be preserved on Live Connection updates, for example, the container of dynamically-loaded content (see mount()).

  • When fromChildNode.liveConnectionUpdate is false, morph() doesn’t remove that fromChildNode even if it’s missing among to’s child nodes. This is useful for elements that should remain on the page but wouldn’t be sent by server again in a Live Connection update, for example, an indicator of unread messages.

  • When fromChildNode.liveConnectionUpdate or any of fromChildNode’s parents is new Set(["style", "hidden", "disabled", "value", "checked"]) or any subset thereof, the mentioned attributes are updated even in a Live Connection update (normally these attributes represent browser state and are skipped in Live Connection updates). This is useful, for example, for forms with hidden fields which must be updated by the server.

Note: to is expected to already belong to the document. You may need to call importNode() or adoptNode() on a node before passing it to morph(). documentStringToElement() does that for you.

Note: to is mutated destructively in the process of morphing. Create a clone of to before passing it into morph() if you wish to continue using it.

Related Work

morph() is different from from.innerHTML = to.innerHTML because setting innerHTML loses browser state, for example, form inputs, scrolling position, and so forth.

morph() is different form morphdom and its derivatives in the following ways:

  • morph() deals better with insertions/deletions/moves in the middle of a list. In some situations morphdom touches all subsequent elements, while morph() tends to only touch the affected elements.

  • morph() supports key="___" instead of morphdom’s id="___"s. keys don’t have to be unique across the document and don’t even have to be unique across the element siblings—they’re just a hint at the identity of the element that’s used in the diffing process.

  • morph() is aware of Live Connection updates, tippy()s, and so forth.

execute()

export function execute(element, event = undefined);

Note: This is a low-level function—in most cases you want to call mount() instead.

Execute the functions defined by the javascript="___" attribute, which is set by @radically-straightforward/build when extracting browser JavaScript. You must call this when you insert new elements in the DOM, for example, when mounting content.

tippy()

export function tippy({
  event = undefined,
  element,
  elementProperty = "tooltip",
  content,
  ...tippyProps
});

Create a Tippy.js tippy. This is different from calling Tippy’s constructor because if tippy() is called multiple times on the same element with the same elementProperty, then it doesn’t create new tippys but mount()s the content.

validate()

export function validate(element);

Validate element (usually a <form>) and its children().

Validation errors are reported with tippy()s with the error theme.

Use <form novalidate> to disable the native browser validation, which is too permissive on email addresses, is more limited in custom validation, and so forth.

You may set the disabled attribute on a parent element to disable an entire subtree.

Use element.isValid = true to force a subtree to be valid.

validate() supports the required and minlength attributes, the type="email" input type, and custom validation.

For custom validation, use the onvalidate event and throw new ValidationError(), for example:

html`
  <input
    type="text"
    name="name"
    required
    javascript="${javascript`
      this.onvalidate = () => {
        if (this.value !== "Leandro")
          throw new javascript.ValidationError("Invalid name.");
      };
    `}"
  />
`;

validate() powers the custom validation that @radically-straightforward/javascript enables by default.

ValidationError

export class ValidationError extends Error;

Custom error class for validate().

validateLocalizedDateTime()

export function validateLocalizedDateTime(element);

Validate a form field that used localizeDateTime(). The error is reported on the element, but the UTC datetime that must be sent to the server is returned as a string that must be assigned to another form field, for example:

html`
  <input type="hidden" name="datetime" value="${new Date().toISOString()}" />
  <input
    type="text"
    required
    javascript="${javascript`
      this.value = javascript.localizeDateTime(this.previousElementSibling.value);
      this.onvalidate = () => {
        this.previousElementSibling.value = javascript.validateLocalizedDateTime(this);
      };
    `}"
  />
`;

serialize()

export function serialize(element);

Produce a URLSearchParams from the element and its children().

You may set the disabled attribute on a parent element to disable an entire subtree.

Other than that, serialize() follows as best as possible the behavior of the URLSearchParams produced by a browser form submission.

reset()

export function reset(element);

Reset form fields from element and its children() using their defaultValue and defaultChecked properties, including calling element.onchange() when necessary.

isModified()

export function isModified(element);

Detects whether there are form fields in element and its children() that are modified with respect to their defaultValue and defaultChecked properties.

You may set element.isModified = <true/false> to force the result of isModified() for element and its children().

You may set the disabled attribute on a parent element to disable an entire subtree.

isModified() powers the “your changes may be lost, do you wish to leave this page?” dialog that @radically-straightforward/javascript enables by default.

relativizeDateTimeElement()

export function relativizeDateTimeElement(
  element,
  { target = element, capitalize = false, ...relativizeDateTimeOptions } = {},
);

Given an element with the datetime attribute, relativizeDateTimeElement() keeps it updated with a relative datetime. See relativizeDateTime(), which provides the relative datetime, and backgroundJob(), which provides the background job management.

Example

html`
  <time
    datetime="2024-04-03T14:51:45.604Z"
    javascript="${javascript`
      javascript.relativizeDateTimeElement(this);
    `}"
  ></time>
`;

relativizeDateTime()

export function relativizeDateTime(dateString, { preposition = false } = {});

Returns a relative datetime, for example, just now, 3 minutes ago, in 3 minutes, 3 hours ago, in 3 hours, yesterday, tomorrow, 3 days ago, in 3 days, on 2024-04-03, and so forth.

  • preposition: Whether to return 2024-04-03 or on 2024-04-03.

localizeDateTime()

export function localizeDateTime(dateString);

Returns a localized datetime, for example, 2024-04-03 15:20.

localizeDate()

export function localizeDate(dateString);

Returns a localized date, for example, 2024-04-03.

localizeTime()

export function localizeTime(dateString);

Returns a localized time, for example, 15:20.

formatUTCDateTime()

export function formatUTCDateTime(dateString);

Format a datetime into a representation that is user friendly.

stringToElement()

export function stringToElement(string);

Convert a string into a DOM element. The string may have multiple siblings without a common parent, so stringToElement() returns a <div> containing the elements.

documentStringToElement()

export function documentStringToElement(string);

Similar to stringToElement() but for a string which is a whole document, for example, starting <!DOCTYPE html>. document.adoptNode() is used so that the resulting element belongs to the current document.

backgroundJob()

export function backgroundJob(
  element,
  elementProperty,
  utilitiesBackgroundJobOptions,
  job,
);

This is an extension of @radically-straightforward/utilities’s backgroundJob() with the following additions:

  1. If called multiple times, this version of backgroundJob() stop()s the previous background job so that at most one background job is active at any given time.

  2. When the element is detached from the document, the background job is stop()ped. See isAttached().

The background job object which offers the run() and stop() methods is available at element[name].

See, for example, relativizeDateTimeElement(), which uses backgroundJob() to periodically update a relative datetime, for example, “2 hours ago”.

isAttached()

export function isAttached(element);

Check whether the element is attached to the document. This is different from the isConnected property in the following ways:

  1. It uses parents(), so it supports tippy()s that aren’t showing but whose targets are attached.

  2. You may force an element to be attached by setting element.isAttached = true on the element itself or on one of its parents.

See, for example, backgroundJob(), which uses isAttached().

parents()

export function parents(element);

Returns an array of parents, including element itself. It knows how to navigate up tippy()s that aren’t showing.

children()

export function children(element);

Returns an array of children, including element itself.

nextSiblings()

export function nextSiblings(element);

Returns an array of sibling elements, including element itself.

previousSiblings()

export function previousSiblings(element);

Returns an array of sibling elements, including element itself.

isAppleDevice

export const isAppleDevice;

Source: https://github.com/ccampbell/mousetrap/blob/2f9a476ba6158ba69763e4fcf914966cc72ef433/mousetrap.js#L135

isSafari

export const isSafari;

Source: https://github.com/DamonOehlman/detect-browser/blob/546e6f1348375d8a486f21da07b20717267f6c49/src/index.ts#L166

isPhysicalKeyboard

export let isPhysicalKeyboard;

Whether the user has a physical keyboard or a virtual keyboard on a phone screen. This isn’t 100% reliable, because it works by detecting presses of modifiers keys (for example, control), but it works well enough.

shiftKey

export let shiftKey;

Whether the shift key is being held. Useful for events such as paste, which don’t include the state of modifier keys.