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

v4.0.7

Published

⚙️ Browser JavaScript in Tagged Templates

Downloads

1,393

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.

Forms in pages using Live Navigation must not use <form>. Instead, they must use <div type="form"> (in block contexts) or <span type="form"> (in inline contexts).

Reasons

  • <form> triggers browser validation, which has some issues, including being too permissive with <input type="email"> (allowing, for example, localhost), being too rigid in custom validation, and so forth. We implementing our own validation system in validate().
  • <form>’s method="___" must be either "GET" or "POST", but <div type="form"> supports any HTTP verb.
  • <div type="form"> sets the CSRF-Protection HTTP header is set, to satisfy @radically-straightforward/server’s CSRF Protection mechanism.
  • <div type="form">s may be nested.

Example

<div type="form" method="PATCH" action="/">
  <input type="text" name="example" placeholder="Name…" />
  <button type="submit">Submit</button>
</div>

Note: <button>s must have an explicit type="submit".

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 to reload the page through an element with key="global-error" which you may style.

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 <div type="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.

The browser JavaScript in javascript="${javascript`___`}" attributes may run on the same element on Live Navigation and on Live Connection updates. If you used something like addEventListener() the same event listener would be added repeated. Instead, you should use something like the onclick property.

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.

Add support for the element.onfocusin and element.onfocusout event handler properties

Unlike most events, browsers don’t support handling the focusin and focusout events with element.onfocusin/element.onfocusout properties—browsers require the use of addEventListener(). We add support for these properties, which are convenient because: 1. They bubble, unlike the related focus and blur events; and 2. Setting event handler properties is idempotent, which is required by javascript="___" snippets.

liveConnection()

export async function liveConnection(requestId, { reload = false } = {});

Open a Live Connection to the server.

If a connection can’t be established, then an error message is shown in an element with key="global-error" which you may style.

If the content of the meta tag <meta name="version" content="___" /> has changed, a Live Connection update doesn’t happen. Instead, an error message is shown in an element with key="global-error" which you may style.

If reload is true then the page reloads when the connection is closed and reopened, because presumably the server has been restarted after a code modification during development.

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 an element with key="global-error" which you may style.

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.

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 element into the contents of the to element with minimal DOM manipulation by using a diffing algorithm.

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

Elements may define a state="___" attribute, typically through the state___() methods below, which is not morphed on Live Connection updates, and is meant to include browser state, for example, whether a sidebar is open.

When morph() is called to perform a Live Connection update (that is,event?.detail?.liveConnectionUpdate is 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 from.liveConnectionUpdate or any of from’s parents is new Set(["state", "style", "hidden", "open", "disabled", "value", "checked"]) or any subset thereof, the mentioned attributes and properties are updated even in a Live Connection update (normally these attributes and properties 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.

  • 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 the server again in a Live Connection update, for example, an indicator of unread messages.

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.

Note: Elements may define an onremove() function, which is called before the element is removed during morphing. This is useful, for example, to prevent leaks of attached IntersectionObservers and MutationObservers by calling IntersectionObserver.disconnect() and MutationObserver.disconnect().

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.

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:

javascript.execute(
  document
    .querySelector("body")
    .insertAdjacentElement(
      "afterbegin",
      javascript.stringToElement(html`<div javascript="___"></div>`),
    ),
);

documentStringToElement()

export function documentStringToElement(string);

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

stringToElements()

export function stringToElements(string);

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

stringToElement()

export function stringToElement(string);

A specialized version of stringToElements() for when the string is a single element and the wrapper <div> is unnecessary.

isModified()

export function isModified(element, { includeSubforms = false } = {});

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 will be lost if you continue.” dialog that @radically-straightforward/javascript enables by default.

reset()

export function reset(element, { includeSubforms = false } = {});

Reset form fields from element and its children() using their defaultValue and defaultChecked properties, including dispatching the input and change events.

validate()

export function validate(element, { includeSubforms = false } = {});

Validate element (usually a <div type="form">) and its children().

Validation errors are reported with popover()s with the .popover--error class, which you may style.

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().

serialize()

export function serialize(element, { includeSubforms = false } = {});

Produce a FormData 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 the behavior of new FormData(form).

relativizeDateTimeElement()

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

Keep an element updated with the relative datetime. See relativizeDateTime() (which provides the relative datetime) and backgroundJob() (which provides the background job management).

Example

const date = new Date(Date.now() - 10 * 60 * 60 * 1000);
html`
  <span
    javascript="${javascript`
      javascript.relativizeDateTimeElement(this, ${date.toISOString()});
      javascript.popover({ element: this });
    `}"
  ></span>
  <span
    type="popover"
    javascript="${javascript`
      this.textContent = javascript.localizeDateTime(${date.toISOString()});
    `}"
  ></span>
`;

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.

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);
      };
    `}"
  />
`;

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, for example, 2024-04-03 15:20 UTC.

stateAdd()

export function stateAdd(element, token);

Add a token to the state="___" attribute

The state="___" attribute is meant to be used to hold browser state, for example, whether a sidebar is open.

The state="___" attribute is similar to the class="___" attribute, and the state___() functions are similar to the classList property. The main difference is that morph() preserves state="___" on Live Connection updates.

The state="___" attribute is different from the style="___" attribute in that state="___" contains tokens which may be addressed in CSS with the [state~="___"] selector and style="___" contains CSS directly.

stateRemove()

export function stateRemove(element, token);

See stateAdd().

stateToggle()

export function stateToggle(element, token);

See stateAdd().

stateContains()

export function stateContains(element, token);

See stateAdd().

popover()

export function popover({
  element,
  target = element.nextElementSibling,
  trigger = "hover",
  remainOpenWhileFocused = false,
  placement = trigger === "hover"
    ? "top"
    : trigger === "click"
      ? "bottom-start"
      : trigger === "none"
        ? "top"
        : (() => {
            throw new Error();
          })(),
});

Create a popover (tooltip, dropdown menu, and so forth).

The target is decorated with the showPopover(), hidePopover(), and positionPopover() functions. The target is decorated with the popoverTriggerElement attribute, which refers to element. The element is decorated with event handler attributes to trigger the popover.

Parameters

  • element: The element that is used a reference when positioning the popover and that triggers the popover open.

  • target: The element that contains the popover contents. It must have the type="popover" type, and it may have one of the .popover--<color> classes (see @radically-straightforward/javascript/static/index.css).

  • trigger: One of the following:

    • "hover": Show the popover on the mouseenter or focusin events and hide the popover on onmouseleave or onfocusout events. The target must not contain elements that may have focus (for example, <button>, <input>, and so forth), otherwise keyboard navigation is broken. On isTouch devices, "hover" popovers don’t show up because they often conflict with "click" popovers.

    • "click": Show the popover on click. When to hide the popover depends on the remainOpenWhileFocused. If remainOpenWhileFocused is false (the default), then the next click anywhere will close the popover—this is useful for dropdown menus with <button>s. If remainOpenWhileFocused is true, then only clicks outside of the popover will close it—this is useful for dropdown menus with <input>s.

    • "none": Showing and hiding the popover is the responsibility of the caller, using the target.showPopover() and target.hidePopover() functions.

  • remainOpenWhileFocused: See discussion on trigger: "click". This parameter is ignored if trigger is something else.

  • placement: One of Floating UI’s placements.

Example

html`
  <button
    type="button"
    javascript="${javascript`
      javascript.popover({ element: this });
    `}"
  >
    Example of an element
  </button>
  <div type="popover">Example of a popover.</div>
`;

Implementation notes

This is inspired by the Popover API and CSS anchor positioning, but it doesn’t follow the browser implementation exactly. First, because not all browsers support these APIs yet and the polyfills don’t work well enough (for example, they don’t support position-try). Second, because the APIs can be a bit awkward to use, for example, asking for you to come up with anchor-names, and using HTML attributes instead of CSS & JavaScript.

We use Floating UI for positioning and provide an API reminiscent of the discontinued Tippy.js. The major difference is that in Tippy.js the content is kept out of the DOM while the popover is hidden, while we keep the target in the DOM (just hidden). This allows, for example, the popover to contain form fields which are submitted on form submission, and it makes inspecting and debugging easier. We also support fewer features and less customization, for example, there isn’t the concept of interactive separate of trigger, so you can’t create an interactive "hover" popover.

parents()

export function parents(element);

Returns an array of parents, including element itself.

children()

export function children(element, { includeSubforms = true } = {});

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.

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’s isConnected is false, the background job is stop()ped.

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”.

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.

isTouch

export let isTouch;

Whether the device has a touch screen, as opposed to a mouse. This is useful, for example, to disable popover()s triggered by "hover". See https://github.com/atomiks/tippyjs/blob/ad85f6feb79cf6c5853c43bf1b2a50c4fa98e7a1/src/bindGlobalEventListeners.ts#L7-L18.