@frostsw/frostjs
FrostJS
A lightweight JavaScript library for DOM manipulation, events, forms and XHR.
Current version: 1.4.1
Installation
From a CDN:
<script src="https://cdn.jsdelivr.net/npm/@frostsw/frostjs@1.4.1/public_html/frostjs.min.js"></script>
With a <script> tag, FrostJS is available globally as FrostJS and its alias fr. FrostJS.version holds the loaded version.
npm (React, Vue, Angular, Vite, webpack)
npm install @frostsw/frostjs
import fr from "@frostsw/frostjs"; // or: import {fr, FrostJS} from "@frostsw/frostjs";
const fr = require("@frostsw/frostjs"); // CommonJS
TypeScript types are included (FrostObject, XhrOptions and the rest can be imported from the package). Importing does not create a global; add window.fr = fr yourself if other scripts expect one. It is safe to import during server-side rendering (Next.js, Nuxt, Angular Universal), as long as FrostJS is only called in the browser.
Use FrostJS after the component is on the page, and pass it elements from refs rather than selectors when you can:
// React
import {useEffect, useRef} from "react";
import fr from "@frostsw/frostjs";
function Profile() {
const form = useRef(null);
useEffect(() => {
const el = fr(form.current);
el.xhr({responseType: "json"}).then(function (response) {
console.log(response);
});
return () => el.off("submit");
}, []);
return <form ref={form} action="/api/profile"><input name="name"/></form>;
}
<!-- Vue -->
<script setup>
import {ref, onMounted} from "vue";
import fr from "@frostsw/frostjs";
const box = ref(null);
onMounted(() => fr(box.value).on("click", e => e.toggleClass("active")));
</script>
<template><div ref="box">Toggle me</div></template>
// Angular
import {AfterViewInit, Component, ElementRef, ViewChild} from "@angular/core";
import fr from "@frostsw/frostjs";
@Component({selector: "app-box", template: `<div #box>Toggle me</div>`})
export class BoxComponent implements AfterViewInit {
@ViewChild("box") box!: ElementRef<HTMLElement>;
ngAfterViewInit() {
fr(this.box.nativeElement)!.on("click", e => e.toggleClass("active"));
}
}
Keep in mind that FrostJS changes the DOM directly: use it for elements the framework does not re-render (or on events and requests), and let the framework own anything it renders from state. The fr.name shortcuts for fr attributes are created on page load; call fr.refresh() after the component renders to use them.
Getting started
Wrap code that touches the page in fr(function () { ... }). It runs once the DOM is ready (immediately if it already is):
fr(function () {
fr("#btnSave").on("click", function (e) {
e.text("Saving...");
});
});
You can have several fr(function () {}) blocks; they run in order.
Selecting elements
fr(selector) accepts a CSS selector, an element, a NodeList/HTMLCollection, an array of elements or another FrostJS object. It returns a FrostJS object wrapping every match, or null when nothing matches.
Two shorthands are added to CSS selectors:
| Shorthand | Becomes | Example |
|---|---|---|
/name |
[fr='name'] |
fr("/login") |
@name |
[name='name'] |
fr("form @email") |
fr attributes
Elements with an fr attribute are registered as properties of fr when the page loads:
<button fr="btnOk">OK</button>
<input fr=""> <!-- an empty fr uses the tag name: fr.input -->
fr.btnOk.text("Hello");
Call fr.refresh() after adding elements with fr attributes to register them (fr.refresh(container) limits it to one container). .fr("name") sets the attribute and registers the element in one step.
Element methods
Setters apply to every matched element and return the FrostJS object, so they can be chained. Getters read the first element.
Traversal
| Method | Description |
|---|---|
.get(index) |
The DOM element at index (default 0). .get("*") returns all of them; .get("latest") re-runs the selector first. |
.element(index) |
Same as .get() without "latest". |
.member(index) |
The element at index wrapped in FrostJS; .member("*") returns an array of FrostJS objects. |
.parent(selector, index) |
Each element's parent, or its closest ancestor matching selector. |
.child(selector) |
All descendants matching selector (all descendants when omitted). |
.forEach(callback) |
Calls callback(member, index) with each element wrapped in FrostJS. |
.filter(callback) |
Keeps the members for which callback returns true. |
.not(selector) |
Removes the members matching selector. |
.is(selector) |
Whether the first element matches selector. |
.length() |
The number of matched elements. |
Content and attributes
| Method | Description |
|---|---|
.html(value) |
Gets or sets innerHTML. |
.self(value) |
Gets or sets outerHTML. |
.text(value) |
Gets or sets textContent. On a <select> it returns the selected option's text. |
.val(value) |
Gets or sets the value. A <select multiple> returns an array. |
.getValue() |
The form value: checked value or true/false for checkboxes and radios, a File or FileList for file inputs, otherwise .val(). |
.attr(name, value) |
Gets or sets an attribute. Pass null to remove it. |
.hasAttr(name, index) |
Whether the element at index (default 0) has the attribute. |
.data(name, value) |
Gets or sets data-name. |
.prop(name, value) |
Gets or sets a DOM property. Pass null to delete it. |
.id(value), .name(value), .fr(value) |
Get or set id, name and fr. |
.num(source, name) |
Reads a number from the value, or from "text", "html", "attr", "css", "data" or "prop" (name is the attribute/style/key). Non-numeric characters are ignored. |
.formatNum(source, name) |
Same as .num(), formatted as 1,234.50. |
.json(source, name, decode) |
Parses JSON from the same sources as .num() (the value by default). Returns {} on invalid JSON. |
Classes, styles and state
| Method | Description |
|---|---|
.class(add, remove) |
Adds and/or removes space-separated classes: .class("active", "inactive"), .class("", "hidden"). With no arguments it returns the first element's classList. |
.hasClass(name) |
Whether any element has the class. |
.toggleClass(name) |
Toggles a class. |
.css(property, value, important) |
Gets or sets an inline style. Pass true as the third argument for !important. |
.visible(value) |
Without arguments, whether the element is displayed. true sets display: block, false sets display: none, a string sets that display value. |
.enabled(value) |
Without arguments, whether the element is not disabled. true/false enables or disables it. |
Adding and removing elements
| Method | Description |
|---|---|
.append(content) |
Appends an HTML string, element, element list or FrostJS object. With several targets, each gets a copy. |
.prepend(content) |
Same as .append(), inserted at the start. |
.clone(index) |
A deep copy of the element at index, wrapped in FrostJS. |
.remove(selector) |
Removes the elements, or only their descendants matching selector. |
fr.create(tagOrHtml) |
Creates an element from a tag name ("div"), an HTML string or a copy of an element. |
fr("#list").append("<li>New item</li>");
fr("#list").append(fr.create("li").text("Another"));
Events
fr(".btn").on("click", function (e) {
alert(e.data("id"));
});
fr("#txtName").on("blur", validateName);
fr("#txtName").off("blur", validateName);
fr("#txtName").off("blur");
fr("#btnSave").trigger("click");
.on(event, callback, useCapture)adds a listener to every element..off(event, callback)removes one listener, or every listener for the event whencallbackis omitted..trigger(event)fires the event.
The event passed to the callback also has all the FrostJS methods of the element the listener is on, so e.val(), e.data("id") and e.class("active") work directly. The FrostJS .data() takes the place of the browser's own data property on input, message and composition events. To read that property, use addEventListener instead.
Input filters
fr("#txtAge").typing(fr.Typing.NUMBERS);
fr.Typing. |
Allows |
|---|---|
LETTERS |
Letters |
NUMBERS |
Digits and a decimal point |
MATH |
Digits and math operators |
MONEY |
Digits, a decimal point and a minus sign |
TITLECASE |
Any text, converted to Title Case as you type |
ALLCAPS |
Any text, converted to UPPERCASE as you type |
Forms
.serialise() collects every named field inside the element:
fr("/formProfile").serialise(); // "name=Jo&email=jo%40mail.com"
fr("/formProfile").serialise(true); // {name: "Jo", email: "jo@mail.com"}
fr("/formProfile").serialise(new FormData()); // FormData, including files
.xhr(options) makes the form submit through fr.xhr() instead of reloading the page, every time it is submitted. It returns a chain with .then() and .catch():
fr("/formProfile")
.configure({headers: {"api-key": apiToken}}) // optional
.xhr({asObject: new FormData(), responseType: "json"})
.then(function (response, form) {
form.class("saved");
})
.catch(function (response, form) {
alert("Could not save");
});
- The request uses the form's
actionandmethod(POST by default) and all its named fields.optionstakes everythingfr.xhr()takes, plusasObjectto choose the format (new FormData()for file uploads,truefor an object).datais added to the form's fields. - Options are merged in this order, later ones winning:
fr.xhr.configure(), then the form's.configure(), then the.xhr()options. .configure(options)is kept on the form elements, so it also applies to laterfr("/formProfile").xhr()calls..configure()returns the form's options, and.configure(null)clears them..then()and.catch()handlers run for every submit, not once, and receive the response and the form..catch()runs on network errors and aborts; HTTP error statuses such as 404 or 500 still go to.then(), as withfr.xhr().- The chain also has
.configure(options)to add options, and.submit()to submit the form right away. - Calling
.xhr()again on the same form replaces its previous setup instead of adding a second handler. successanderroroptions still work.
XHR
fr.xhr(options) sends a request and returns a Promise:
fr.xhr({
url: "api/save.php",
method: "POST",
data: {name: "Jo", tags: ["a", "b"]},
responseType: "json"
}).then(function (response) {
console.log(response);
});
| Option | Description |
|---|---|
url |
Required. |
method |
"POST" by default. For GET, the data is added to the query string. |
data |
An object (nested objects and arrays are supported), a query string or FormData. |
headers |
An object of request headers. |
async, user, pass, mime |
Passed to open() and overrideMimeType(). |
progress |
Upload progress callback. |
success, error, abort |
Callbacks, in addition to the Promise (which rejects on error and abort). |
config |
false to skip the presets below. |
| any other | Set on the XMLHttpRequest itself, e.g. responseType, timeout, withCredentials. |
Requests are sent as application/x-www-form-urlencoded unless the data is FormData or you set a Content-Type header. A request without headers also sends api-key: secret-key. The last request is available as fr.xhr.instance.
Presets
fr.xhr.configure() sets options that every request uses, including form submits:
fr.xhr.configure({
headers: {"api-key": apiToken},
data: {csrf: csrfToken},
timeout: 30000
});
- Each call adds to the existing presets.
fr.xhr.configure()with no argument returns them. - Options passed to a request win. Headers are matched by name regardless of case, and preset data is merged with the request's data (objects by key;
FormDataand query strings get the missing fields). fr.xhr.config = {...}also sets presets and takes priority overconfigure().
Hooks
fr.xhr.initdata = [{name: "csrf", value: token}]; // added to every request's data
fr.xhr.beforesuccess = function (response) { }; // runs before every success callback
Both can also be set with fr.regedit("initdata", [...]) and fr.regedit("beforesuccess", fn).
Plugins
fr.plugin(name, fn) adds a method to every FrostJS object. Inside fn, this is the FrostJS object:
fr.plugin("highlight", function (color) {
this.css("background-color", color || "yellow");
return this;
});
fr("#note").highlight();
Plugins attach to FrostJS objects created after the plugin is registered. The fr.name shortcuts are created when the page loads, so call fr.refresh() after adding a plugin if you use them, or register plugins before the page finishes loading.
A plugin never replaces an existing method. If the name is already used by a built-in method or another plugin, the plugin is not added, a warning is logged and fr.plugin() returns false.
Overriding a method
To replace a built-in method or a plugin on purpose, use fr.override(target, fn). target is the method name or the method itself. Inside fn, this.<name>.original(...) calls the method being replaced:
fr.override("data", function (name, value) {
return this.data.original(name, value);
});
fr.override(fr("#note").highlight, function () {
return this.highlight.original("lightblue");
});
Overrides stack: each one wraps the previous one. fr.override() returns false if there is no method by that name.
Other functions
| Function | Description |
|---|---|
fr.regedit(path, value) |
Stores a value in the shared registry; without value, reads it. Paths use ->, e.g. fr.regedit("user->name", "Jo"). fr.regedit("*") returns the whole registry. |
fr.cookie(name) |
Reads a cookie; returns false when it is not set. |
fr.cookie(name, value, maxAge, sameSite) |
Sets a cookie. maxAge is in seconds (1 day by default), sameSite is "Lax" by default. |
fr.num(value) |
Extracts a number from a string: fr.num("$1,250.50") → 1250.5. |
fr.formatNum(value) |
Formats a number with two decimals and thousands separators: "1,250.50". |
fr.split(value, delimiter, removeEmpty) |
Splits a string, dropping empty parts unless removeEmpty is false. |
fr.serialise(object) |
Serialises an object into a query string. |
fr.getObjectSize(object) |
The number of own properties. |
fr.noConflict() |
Points window.fr to FrostJS and returns a function that restores the previous fr. |
What's new in 1.4
- XHR presets:
fr.xhr.configure()and theconfigoption. - Form XHR:
fr(form).configure({...}).xhr({...}).then(...).catch(...). - npm modules:
import/requiresupport for bundlers and Node, and TypeScript types. fr.override(), and plugins can no longer silently replace built-in methods or other plugins.- Events: handlers for
input,messageandcompositionevents no longer crash, ande.data()works on every event. - Fixes:
fr(function)runs immediately when the page has already loaded..off()removes listeners properly..visible()reads the computed style..append()/.prepend()keep order and copy content to every target..parent(selector)skips elements without a match.- Cookies are encoded and default to
SameSite=Lax. fr.create()copies the element it is given.- XHR serialises nested data and sends GET data in the query string.
- Reassigning an array with
fr.regedit()orfr.xhr.initdatareplaces it instead of keeping the old entries, so rotating CSRF tokens are sent correctly.
License
ISC
Related Packages
Browser-native library to fetch GitHub repositories and custom metadata for dyna...
VaultDrop SDK for browsers — Web Crypto AES-GCM and API client for React, Vue, A...
Storage is plugin that help to store data in the local storage, or retrieve data
Version History
| Version | Released | License |
|---|---|---|
| 1.4.1 | ISC | |
| 1.3.12 | ISC | |
| 1.2.21 | ISC | |
| 1.2.11 | ISC | |
| 1.2.10 | ISC | |
| 1.2.9 | ISC |