A utility library for "what Native JS should have had"
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:
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
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+');
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
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*/)
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;
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;
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"}]
]]])
);
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
Wyvern.js provides powerful data binding similar to React, with both declarative (HTML) and imperative (JavaScript) approaches.
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)">
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);
});
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);
});
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.
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++;
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()
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:
query.paramName (returns the value).query = { paramName: 'newValue' }.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.
persist.key = value.persist.key (returns the value or undefined if not set).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).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).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.el.onceel.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 chainingel.replaceWith(otherElement: HTMLElement) replaces element, returns otherElement for chainingMISC:
data-bind attribute for declarative data binding with the following pattern: "targetElementPropertyPath=sourceElementId.sourceElementPropertyPath+optionalJavaScriptExpression".
<span data-bind="style.color=otherElId.value+'#'+v">. Here, v represents the current value of otherElId.value..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 objectsfw.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)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)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)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)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).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)el.onceel.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 chainingel.replaceWith(otherElement) replaces element, returns otherElement for chainingMISC:
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:
firewyrm or fw (because typing firewyrm so many times is too difficult)fw.mock(()=>{}).replace(window [or any other obj], "prop")fw.start()fw.end()fw.mock.restore() restores the objects. [of course]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();