Wyvern.js

A utility library for "what Native JS should have had"

Introduction

10kB lightweight, comprehensive & robust, fast & lean.

Documentation

Wyvern.js (also called reducepain.js) is a utility library that adds pretty nice functionality to native JavaScript, filling (percieved) gaps in the standard API.

It provides:

  • Better DOM manipulation utilities
  • Improved timing functions
  • Form validation and data conformity tools
  • React-like reacive bindings, without overhead
  • Redux-like persistant storage, but simpler
  • Device/browser detection
  • State management
  • A lightweight testing framework

All functionality is automatically available when you include the script. All you need is a <script src="wyvern.min.js">

Get it here: Minified version, with comment-based docs or, if you want to change things, Unminified verison, with (snarky) comments

Core Features

DOM Selection

Enhanced selector functions that intelligently choose between getElementById and querySelector:

// Simple ID
const el = $('myElement');

// CSS selector
const btn = $('.btn-primary');

// Query all
const allDivs = $('div+');

Element Prototypes

Added convenience methods to all elements:

// Shorthand for querySelector
el.qs('.child');

// Chained styling
el.css({color: 'red', fontSize: '20px'})
  .thendo(() => console.log('Done!'));

// Data binding
input.when('value').change(div, 'textContent');

// Automatic classes/attrs
input.attrs.max = 20;
input.attrs.required = undefined;

input.classes.show = true;
console.log(input.classes.show) //true
Hover over me

Improved Timing

Better alternatives to setInterval:

// Run a task every second
const taskId = setTask(() => {
    console.log('Running');
}, 1000);


// Set to run every 500 ms
changeTask(taskId, {duration: 500}) 

// Stop task
cancelTask(taskId);

// Advanced usage:
setTask((arg1, arg2)=>{
    console.log(arg1, arg2);
}, 1000, [arg1, arg2], 5 /*run 5 times*/)
Counter: 0

Persistent Storage

Easy localStorage access:

// Set a persistent value
persist.theme = 'dark';

// Get it back later
console.log(persist.theme); 
// 'dark', still 'dark' even after reload

// Remove it
persist.theme = undefined;

URL Query Params

Easy access to URL search parameters:

// Set a parameter
query.page = '2';

// Get a parameter
const page = query.page;

// Delete a parameter:
query.page = undefined;
Current URL parameters:

Element Creation

JSX-like element creation with tree() (includes component support):

const myElement = tree(
    ['div', {class: 'card'}, [
        ['h3', {}, 'Title'],
        ['p', {}, 'Content']
    ]]
    function DivComponent(params, children){
        return ['div', {style:`color:${params.color}`}, children];
    }
    function Sections(params, children){
        return children.map(e=>['div', {class: "section"}, e]);
    }
    const sectionedDivs = tree([[Sections, {}, [
        [DivComponent, {color: "green", text:"hi"}],
        [DivComponent, {color: "red", text:"bye"}]
    ]]])
);

Validation & Conformity

Wyvern.js provides validation and data conformity utilities:

Validation functions:

validate.email('test@example.com'); // true
validate.password('Weak1'); // false
validate.creditCard('4111111111111111'); // true

Data conformity functions:

conform.phone('1234567890'); // (123) 456-7890
conform.date('2023-01-01'); // 2023-01-01
conform.creditCard('4111111111111111'); // 4111 1111 1111 1111

React-Like Bindings

Wyvern.js provides powerful data binding similar to React, with both declarative (HTML) and imperative (JavaScript) approaches.

Declarative Binding with data-bind

Use data-bind attributes in your HTML for simple bindings:

<!-- Basic value binding -->
<input id="nameInput" type="text">
<span data-bind="textContent=nameInput.value"></span>

<!-- With transformation -->
<input id="colorInput" type="text">
<div data-bind="style.backgroundColor=colorInput.value+'#'+v"></div>

<!-- Pipe to a function -->
<input data-bind="value=+console.log(v)">

Simple Text Binding

Style Binding with Transformation

Enter color without # (e.g., 'FF0000'):

Multiple Bindings

Imperative Binding with .when()

Use JavaScript for more complex bindings with .when().change() and .when().tell():

// Simple value binding
input.when('value').change(output, 'textContent');

// With transformation
input.when('value', v => v.toUpperCase())
     .change(output, 'textContent');

// With callback
input.when('value').tell(value => {
    console.log('Value changed:', value);
});

.when().change() Binding

.when().tell() Callback

Advanced Binding Patterns

Combine techniques for powerful data flows:

// Two-way binding
const input1 = $('#input1');
const input2 = $('#input2');

input1.when('value').change(input2, 'value');
input2.when('value').change(input1, 'value');

// Complex transformation
priceInput.when('value', v => parseFloat(v) || 0)
          .tell(price => {
              taxOutput.textContent = (price * 0.2).toFixed(2);
              totalOutput.textContent = (price * 1.2).toFixed(2);
          });

Two-Way Binding

Calculated Values

Tax (20%):
Total:

Store Integration

Store value:

Device Detection

Wyvern.js provides comprehensive device and browser information:

device.isMobile; // true/false
device.browser.isChrome; // true/false
device.os.isWindows; // true/false
device.screen.width; // screen width
device.connection.effectiveType; // '4g', '3g', etc.

State Management

Simple state management with stores:

const store = createStore({count: 0});

// Watch for changes
store.when('count').tell((value) => {
    console.log(`Count is now ${value}`);
});

// Modify state
store.count++;

Testing Framework

Wyvern.js includes a lightweight testing framework, with changable logs, quick mocks, and progress callbacks (Firewyrm):

fw.start();

// add api mock
fw.mock(()=>return {data:1}).replace(window, "fetch");

fw.section('Math Tests')
.test('Addition', () => 1 + 1 === 2)
.assert('Multiplication', () => 2 * 3).is(6);

fw.section('Async Tests')
.test('Async operation', async () => {
    await new Promise(r => setTimeout(r, 100));
    return true;
})
.assert('Delayed value', async () => {
    await new Promise(r => setTimeout(r, 50));
    return 42;
}).is(42);

fw.test('Standalone test', () => true);

fw.end()

// restore mocks
fw.mock.restore()

Docs

Documentation for wyvern.js (reducepain.js)

(This is the clean version, without snarky comments)

Exposed Functions/Objects (to window):

  • $(selector: string): Selects a single element using getElementById if the selector is not a css statement, otherwise uses querySelector. Append a + to the selector string (e.g., '.item+') to use querySelectorAll.
  • setTask(func: Function, [args: any[]], [delay: number = 0], [iters: number = Infinity]): number: Sets up a recurring task similar to setInterval. Returns an ID to manage the task. Also provides:
    • cancelTask(id: number): Cancels the task with the given ID.
    • changeTask(id: number, { delay: number }): Modifies the delay of the task with the given ID.
  • window.body: HTMLElement: A direct reference to the document's <body> element.
  • tree([]: any[]): HTMLElement: Takes a nested array structure representing DOM elements, constructed in a JSX-like format: ['elementName', { attribute: 'value' }, [...children]], returns HTMLElement constructed from the array.
  • window.urlQuery: URLSearchParams: An automatically generated URLSearchParams object for the current window's URL.
  • window.query: object: An object for interacting with URL query parameters:
    • Getting a parameter: query.paramName (returns the value).
    • Setting/changing a parameter: query = { paramName: 'newValue' }.
    • Deleting a parameter: query = { paramName: undefined }.
  • window.validate: object: An object containing various validation functions:
    • validate.required(value: any): boolean (checks if the value is empty).
    • validate.email(value: string): boolean.
    • validate.phone(value: string): boolean.
    • validate.alphabetic(value: string): boolean.
    • validate.numeric(value: string): boolean.
    • validate.password(value: string): boolean (requires 8 characters, 1 number, 1 uppercase, 1 special character).
    • validate.date(value: string): boolean.
    • validate.url(value: string): boolean.
    • validate.creditCard(value: string): boolean.
    • validate.zipCode(value: string): boolean.
  • window.conform: object: An object containing functions to attempt to conform strings to specific formats:
    • conform.phone(value: string): string | false (returns the conformed string or false if unable).
    • conform.date(value: string): string | false.
    • conform.url(value: string): string | false.
    • conform.email(value: string): string | false.
    • conform.alphabetic(value: string): string | false.
    • conform.creditCard(value: string): string | false.
    • conform.simplePhone(value: string): string | false (returns plain numbers).
    • conform.simpleCreditCard(value: string): string | false (returns plain numbers).
  • window.fetchJson(url: string, [options: RequestInit]): Promise<any>: Performs a fetch request and directly returns the parsed JSON response.
  • window.fetchText(url: string, [options: RequestInit]): Promise<string>: Performs a fetch request and directly returns the text response.
  • window.persist: object: An object for simple persistent storage across page loads using localStorage.
    • Setting a value: persist.key = value.
    • Getting a value: persist.key (returns the value or undefined if not set).
    • Removing a value: persist.key = undefined.
  • window.device: object: An object containing various device and browser information:
    • istouch: boolean.
    • touchpoints: number.
    • islightmode: boolean.
    • isdarkmode: boolean.
    • isoffline: boolean.
    • connection: object (.effectiveType: string, .saveData: boolean, .downlink: number, .rtt: number).
    • isMobile: boolean.
    • isDesktop: boolean.
    • browser: object (.isChrome: boolean, .isFirefox: boolean, .isSafari: boolean, .isEdge: boolean, .isIE: boolean).
    • os: object (.isWindows: boolean, .isMac: boolean, .isLinux: boolean, .isiOS: boolean, .isAndroid: boolean).
    • screen: object (.width: number, .height: number, .orientation: string, .pixelRatio: number).
    • viewport: object (.height: number, .width: number).
    • battery: Promise<BatteryManager | null>.
    • hasGeolocation: boolean.
  • createStore(): object: Creates a simple state management store.
    • store.when(key: string | undefined, [preprocess: Function]): object: Attaches a listener to a specific key (or all keys if undefined).
    • .tell(someFunction: Function): Executes someFunction(value, store) when the specified key changes. If no key is specified in when(), it executes someFunction({ key: value }, store).
    • The store can be modified directly like a regular JavaScript object.

Stuff I prototype'd onto elements:

  • el.$(id: string): HTMLElement | null: Equivalent to el.getElementById(id).
  • el.qs(selector: string): Element | null: Equivalent to el.querySelector(selector).
  • el.qsa(selector: string): NodeListOf<Element>: Equivalent to el.querySelectorAll(selector).
  • el.link(propertyPath: string, transformFunction: Function): object: Creates a reactive binding between an element's property and a value. Example: color = el.link('style.color', v => '#' + v). Now, setting color.v = 'FF0000' will update el.style.color.
  • input.when(attributeName: string, transformFunction: Function): object: Sets up a listener for changes on the specified attribute.
    • .change(targetElement: HTMLElement, targetPropertyPath: string): Updates the targetElement's targetPropertyPath with the transformed value. Example: input.when('value', v => v.toUpperCase()).change(div, 'innerText').
    • .tell(someFunc: Function): Executes someFunc(transformedValue) when the attribute changes. Example: input.when('value', v => v.toUpperCase()).tell(console.log).
  • Nested attributes can be accessed using dot notation in propertyPath (e.g., 'style.color').
  • el.css(styles: object): HTMLElement: Applies the provided CSS styles to the element and returns the element. Example: el.css({ color: 'red', backgroundColor: 'yellow' }).
  • el.thendo(eventType: string | string[], callback: Function): HTMLElement: Executes the callback function after the specified CSS transition, animation, or media playback ends. Example: el.thendo('transitionend', () => console.log('Transition finished')).
  • el.on(type: string | Array[string...], listener: EventListenerOrEventListenerObject, [options: AddEventListenerOptions | boolean]): void: A shorthand for el.addEventListener(), or a multi-addeventlistener.
  • Same style with el.once
  • el.off(eventType: string | string[], [specificCallback: Function]) remove event listeners. Only works with ones added via .on or .once.
  • el.classes: Proxy | Boolean an object used to interact with an element's classes. Do el.classes.name = true/false to set, and el.classes.name will also represent the current state.
  • el.attrs an object used to interact with an element's attributes, similar to window.query.
  • el.show(optionalDisplayMode: string), el.hide(), el.toggle(optionalDisplayMode: string) Saves the previous displayMode on hide.
  • el.animateTo(animations: arr | object, duration: number, [easing: string], [iterations: number]) animates an element according to a list of animations.
  • el.appendTo(parentElement: HTMLElement), el.prependTo(parentElement: HTMLElement) appends/prepends, returns el for chaining
  • el.replaceWith(otherElement: HTMLElement) replaces element, returns otherElement for chaining

MISC:

  • Elements can use the data-bind attribute for declarative data binding with the following pattern: "targetElementPropertyPath=sourceElementId.sourceElementPropertyPath+optionalJavaScriptExpression".
    • Example: <span data-bind="style.color=otherElId.value+'#'+v">. Here, v represents the current value of otherElId.value.
    • You can pipe the value to a function (similar to .watch().tell) using a +: <input data-bind="value=+console.log(v)"> (in this case, v is the input element's value).

SIMPLE TESTS WITH FIREWYRM:

  • fw or firewyrm is the global object for testing.
  • fw.start(): Begins a new test block.
  • .section(name: string): Adds a named section to group tests.
  • .test(name: string, assertionFunction: Function): object: Adds a test with a boolean assertion.
  • .assert(name: string, valueFunction: Function): object: Adds an assertion that returns a value.
  • .is(expectedValue: any): Checks if the returned value equals the expectedValue.
  • .test(name: string, asyncAssertionFunction: Function): Promise<void>: Supports asynchronous tests.
  • .assert(name: string, asyncValueFunction: Function): object: Supports asynchronous assertions.
  • .is(expectedValue: any): Checks if the resolved value equals the expectedValue.
  • fw.test(name: string, assertionFunction: Function): Adds a standalone test.
  • fw.end(): Ends the current test block and likely displays the results.
  • fw.mock(Function).replace(obj, prop: string): setups a mock, replaces some function with it.
  • fw.mock.restore(): Restore mocked objects
  • Example:
fw.start();

fw.section('Math Tests')
.test('Addition', () => 1 + 1 === 2)
.assert('Multiplication', () => 2 * 3).is(6);

fw.section('Async Tests')
.test('Async operation', async () => {
await new Promise(r => setTimeout(r, 100));
return true;
})
.assert('Delayed value', async () => {
await new Promise(r => setTimeout(r, 50));
return 42;
}).is(42);

fw.test('Standalone test', () => true);

fw.end();

Documentation for wyvern.js (reducepain.js)

(This is the original, with all the unfiltered thoughts)

Exposed Functions/Objects (to window):

  • $('id/selector') will choose getElement or querySelector when most applicable. End the selector string with a + to querySelectorAll (because who can remember which one to use when? not me)
  • setTask(func, [args], delay = 0, iters = Infinity) => id and also cancelTask(id) / changeTask(id, {delay: number}), a setInterval alternative (setInterval is kinda janky, so here's a better way, maybe)
  • body is in window (because typing document.body is for chumps)
  • tree() returns a list of elements constructed in a JSX like manner ['el name', {attr: prop}, [... children (same array format)]] (kinda like JSX but without the transpiler headache, you're welcome)
  • a auto-generated URLSearchParams object for the window named "urlQuery" (finally, easy access to those URL params)
  • window.query object that returns the query params when get, you can also do query={key: value} to set/change a key/value pair. Set value to undefined to delete (getting and setting query params shouldn't be this hard, so I fixed it)
  • validate.{validationScheme}, available schemes: required (isempty), email, phone, alphabetic, numeric, password (8 chars, 1 number, 1 uppercase letter, 1 special character), date, url, creditCard, zipCode (because validating user input is like 90% of web dev, might as well make it easy)
  • conform.{conformingScheme}, takes a string and tries to conform it, outputs false if unable. Has schemes: phone, date, url, email, alphabetic, creditCard, simplePhone, simpleCreditCard (simple returns plain numbers) (sometimes you just want to make the input look right, even if the user messed up)
  • fetchJson/fetchText, fetch without the .text() / .json() callback needed (because promises are cool, but extra steps are not)
  • a persist object, that if you set something like persist.theme = 'dark', next page load, persist.theme will be dark still. If never set, will return undefined, and to "remove", set it to undefined (local storage, but way less typing, because I'm lazy)
  • a window.device object, with a bunch of datas: istouch, touchpoints, islightmode, isdarkmode, isoffline, connection (.effectiveType, .saveData, .downlink, .rtt), isMobile, isDesktop, browser (.isChrome, .isFirefox, .isSafari, .isEdge, .isIE), os (.isWindows, .isMac, .isLinux, .isiOS, .isAndroid), screen (.width, .height, .orientation, .pixelRatio), viewport.height, viewport.width, battery, hasGeolocation (basically everything you'd ever want to know about the user's device, all in one place)
  • createStore() will create a store. You can attach to the store: store.when('key', [preprocess]).tell(someFunction) //two args: value and whole store wildcard: store.when().tell(someFunction) //two args: {key: value} and whole store You can modify the store as normal (simple state management, because Redux is overkill for like, everything)

Stuff I prototype'd onto elements:

  • .$ will getElementById on an element (yes, like el.$()) (because why not?)
  • .qs / .qsa = .querySelector / .querySelectorAll (on all elements) (consistency is key, even if it's slightly weird)
  • you can do color = el.link('style.color', v=>'#'+v), and now color.v = 'FF0000' will set the color correctly (two-way binding without the framework drama)
  • input.when('value', v=>v.toUpperCase()).change(div, 'innerText') will bind the value attr to innerText attr (make elements talk to each other, it's fun)
  • input.when('value', v=>v.toUpperCase()).tell(someFunc) will bind to a function (do stuff when things change, the core of interactivity)
  • for all of them, nested attrs can be accessed by: 'style.color' (because traversing nested objects is annoying)
  • .css({prop: style}) will change the styles, returns the element passed through (because setting inline styles one by one is for robots)
  • .thendo() will wait for transisionend/other animation ending/playback ending and run the function (because timing is everything in UI)
  • .on() replaces .addEventListener() - or, .on([str...]) for multiple-in-one. (because less typing is always better)
  • Same style with el.once
  • el.off(string or array of eventTypes, [optional callbackfunction to specify for]) remove event listeners. Only works with ones added via .on or .once, though, so remember to not use addEventListener()
  • el.classes an (magic) object used to interact with an element's classes. Do el.classes.name = true/false to set, and el.classes.name will also represent the current state.
  • el.attrs simplifies setting element's attributes, similar to window.query.
  • el.show(optional string to set display to), el.hide(), el.toggle(optional string to set display to) Saves the previous displayMode on hide, too, for niceness.
  • el.animateTo(an normal .animate object OR an array of them, duration, [easing (string)], [iterations]) animates an element according to a list of animations.
  • el.appendTo(HTMLElement), el.prependTo(HTMLElement) appends/prepends, returns el for chaining
  • el.replaceWith(otherElement) replaces element, returns otherElement for chaining

MISC:

  • Elements can use the data-bind attribute in this pattern: "some.element.option=otherElementId.value+optionalJsExp", as in <span data-bind="style.color=otherElId.value+'#'+v"> (the code provides v as the input from otherElId.value) will change the element's color based on otherElId's value, prefixing a #. You can pipe it to a function, as in .watch().tell, by: <input data-bind="value=+console.log(v)"> (this is a bit backwards - the v is from this element's value) (declarative binding in HTML? kinda neat, right?)

SIMPLE TESTS WITH FIREWYRM:

  • it is either firewyrm or fw (because typing firewyrm so many times is too difficult)
  • add mocks by fw.mock(()=>{}).replace(window [or any other obj], "prop")
  • start a test block with fw.start()
  • add tests/asserts/sections
  • then call fw.end()
  • fw.mock.restore() restores the objects. [of course]
  • EX:
fw.start();

fw.section('Math Tests')
.test('Addition', () => 1 + 1 === 2)
.assert('Multiplication', () => 2 * 3).is(6);

fw.section('Async Tests')
.test('Async operation', async () => {
    await new Promise(r => setTimeout(r, 100));
    return true;
})
.assert('Delayed value', async () => {
    await new Promise(r => setTimeout(r, 50));
    return 42;
}).is(42);

fw.test('Standalone test', () => true);

fw.end();