Documentation & Tests

Objs is an open-source library that can get and change DOM elements, control their states and events, send AJAX requests, load and cache scripts, styles, images and test functions (unit and async tests). Also it is just 10KB with no dependencies.

Usage

Just include the script or import with NPM and call any function you need. Use the left or bottom navigation (on mobile) to find topic you need or study examples on the main page.

Base

Selector / Getting DOM elements

Use o('selector') to select elements like querySelectorAll. If you need only the first element use o.first(selector). o.take(q) gets elements like o(q); if the number of matches equals the number of previously inited components, it returns those inited Objs. Also it can get a DOM element or an array of elements o(elements) or self object in state. Function returns object with Objs methods and DOM elements - more info below.

// gets all elements and returns Objs object
const objElements = o('.class');
// gets the first element only
const objElement = o.first('.class');
// gets new elements to operate
objElements.reset('.class2');
// query to find all children of each element by selector
objElement.find('.childClass');
// returns only the first child of each element
objElement.first('.childClass');
// give an element or an array[] of elements to set them for operating
const objElement = o(document.getElementById('name'));

o.take(query)

o.take(q) selects elements by q (selector, DOM element, or array). If the selected elements are inited components (they have data-o-init from o.init().render()), o.take(q) returns the corresponding ObjsInstance(s) so you can call state methods. If the number of matches equals one inited component's elements, you get that component; otherwise you get a plain Objs wrapping the DOM elements. Use it to re-bind to components already in the DOM (e.g. after navigation or dynamic insert).

// select by selector — get DOM elements or inited components
const items = o.take('.list-item');  // Objs of .list-item elements

// when elements were created with o.init().render(), take() returns those components
const cardStates = { render: { tag: 'div', class: 'card', html: '...' }, update: ({ self }, t) => { self.html(t); } };
o.init(cardStates).render().appendInside('#container');
// later: get the inited component back and call state methods
const card = o.take('.card');  // if single .card with data-o-init, card is ObjsInstance with .update()
if (card.update) card.update('New content');

// o(number) — get inited component by index
const firstInited = o(0);  // same as o.inits[0]

There are some ways to get DOM elements from Objs if you need:

// returns all selected DOM elements as array [el0, ..]
o('.class').els;
// number of elements
o('.class').length;
// the first DOM element itself
o('.class').el;
// the last DOM element
o('.class').last;

// Select elements from set
// sets the i DOM element for operations (starts from 0 index)
objElements.select(i).el;
// sets the last item to operate
objElements.select().el;
// sets all DOM elements to control again
objElements.all().els;

To remove selected elements from DOM or control Objs list use this:

// removes all elements from DOM
objElements.remove();
// removes i-index element from DOM and returns Objs
objElements.remove(i);
// adds element to operation list and sets oInit attribute to parent's (works for existing DOM elements only)
objElements.add(element);
// it is possible to use selector or an array of elements
objElements.add('.class2');
// removes i-index element from list of objects to operate (not from DOM)
objElements.skip(i);

// v2.0: new method to delete DOM and inited Objs object
objElements.unmount();

If you have inited some events by .on(), you should init them for added elements by .onAll(). If you need to disable all events, e.g when a modal window is showed, use .offAll(). It just disable events, so .onAll() will enable them again.

objElements.add(element).onAll();
objElements.offAll();
Testing

Direct changing DOM elements

There are several methods to change all elements or some of them (by using .select(), .skip(), etc.)

Changing attributes

// any attributes
// setting
o(selector).attr('attr', 'value');
// getting
o(selector).attr('attr');
// removing (v2.0: null removes, '' sets empty string)
o(selector).attr('attr', null);
o(selector).attr('attr', '');
// getting attributes as an array from all elements [{}, ...]
const attributes = o(selector).attrs();
// getting attributes of selected element {}
const attributes = o(selector).select(3).attrs();

// dataset
// setting
o(selector).dataset({
	action: "registration"
});
// getting as an array from all elements [{}, ...]
const datasets = o(selector).dataset();
// getting of selected element {}
const dataset = o(selector).select(3).dataset();

// style 
// setting attribute as a string
o(selector).style('value');
// setting as object (use "" for hyphenated names)
o(selector).css({
	height: "100px",
	"max-width": "100px"
});
// remove style attribute entirely (v2.0)
o(selector).style(null);
o(selector).css(null);

// getting inner HTML of all elements in one string
o(selector).innerHTML();

There are special functions to controll class attribute.

// direct setting value of attribute (replaces entire class string)
o(selector).setClass('class1 class2');
// add one or more classes (v2.0: spread)
o(selector).addClass('class');
o(selector).addClass('active', 'highlight', 'loaded');
// remove one or more classes
o(selector).removeClass('class');
o(selector).removeClass('active', 'hidden');
// switching having and not having "class"
o(selector).toggleClass('class');
// switch by condition (true = add, false = remove)
o(selector).toggleClass('class', isActive);

// returns true if all (or selected) elements have the class
const hasIt = o(selector).haveClass('class');

To append HTML or elements to DOM you can use this functions. Append functions get selector or a DOM element.

// setting inner HTML
o(selector).innerHTML('html');
o(selector).html('html');
// get HTML (no argument) — string of first element, or concatenated for multiple
o(selector).html();
// clear content (empty string)
o(selector).html('');
// inner text
o(selector).innerText('text');
// inner text content
o(selector).textContent('text');

// creating element for example below, more info in section States control
const objs = o.init(states).render();

// append as the last child/children of the element
// or in the first found by selector
objs.appendInside('#root');
// append before, after
objs.appendBefore(element);
objs.appendAfter('#child');

If you need to operate elements, use .forEach(function) - it gets the same parameters as a state: o, self, i-index and el for each element.

o(selector).forEach(({self, i, el, o}) => {
	// self.els[i] - each controlled element
});

There is a flag to enable debug console output on an object or globally.

o.debug = false;// set to true for console log globally
o(...).debug()...;// insert debug() method to enable debug in the object
});

val([value]) — get or set the .value of input, textarea, select. Call without argument to get current value; with argument to set and return Objs for chaining. Intended for input, textarea, select; behavior on other elements is undefined.

// get value (first element if multiple)
o('input').val();

// set value and chain
o('input').val('new text');

// clear / remove value
o('input').val('');
o('#search').first('input').val('').attr('placeholder', 'Search...');  // chain with other methods
Testing

val(), refs, className and DOM extras

refs — after init(), every child with ref="name" is available as component.refs.name (ObjsInstance). Use className in render state as alias for class. addClass / removeClass accept multiple arguments. css(null) or style(null) removes the style attribute.

// refs: access named children without selectors
const formStates = {
  render: { tag: 'form', html: '' },
  setEmail: ({ self }, v) => { self.refs.email.val(v); },
  disable: ({ self }) => { self.refs.submit.attr('disabled', 'true'); }
};
const form = o.init(formStates).render();
form.refs.email.val('user@example.com');
form.refs.submit.el;  // raw DOM button
Testing

React and JSX

To create a rendered Component for JSX or React element from Objs use .prepareFor(). The first parameter should be React or createElement function. In v2.0 the second parameter is omitted, Component with createElement('div') will be returned. In v.1.1 was the second React.Component parameter to get Component as return.

.prepareFor(React) returns a Component that creates div element and inserts HTML or elements inside. States can be swiched by useEffect() hook or properties changing.

To set "on" events init useRef and give ref in properties to the Component. In this case Objs will append DOM elements inside div element with "on" events. Objs automatically converts properties like "onClick" to "click" and adds listeners.

import o from 'objs-core';
import React from 'react';
import { useRef } from 'react';

// Simple example
const root = ReactDOM.createRoot(o('#root').el);
root.render(o.init(state).prepareFor(React));

// Component example
const MyObjsComponent = o.init(state).prepareFor(React);

function App() {
	const ref = useRef(null);
	const clickHandle = () => {
		...
	};

	return (
		<div>
			{/* use as a Component */}
			<MyObjsComponent
				name="Roman"
				src="/profile"
				onClick={ clickHandle }
				ref={ ref } />
		</div>
	);
}

To use Objs in React project, place states objects in components folder and export prepared Components.

// Component file example
import o from 'objs-core';
import React from 'react';

export const objsForm = {
	render: {
		...
	},
	loaded: {
		...
	}
};

export const Form = o.init(objsForm).prepareFor(React);

QA autotag and reactQA

o.autotag — set to a string (e.g. "qa") to auto-add data-{autotag}="component-name" to all rendered elements. Component name comes from states.name (camelCase to kebab-case). o.reactQA(componentName) returns { 'data-qa': 'kebab-name' } (or data-{autotag} if set) for spreading onto React JSX. Converts CamelCase to kebab-case.

o.autotag = 'qa';
// Objs rendered elements get data-qa="my-component"
// React:
<button {...o.reactQA('CheckoutButton')}>Checkout</button>
// → data-qa="checkout-button"
Testing

Events

There are synonyms for standart event methods but they can get multi events separated by ', ' (comma and space). Check event.target (e.g. its classList) in the listener to check if common document event started by the right element.

// synonym for addEventListener but with multi events
o(selector).on('event1, event2', listener, options);
// multi events removing (removeEventListener)
o(selector).off('event1, event2', listener, options);

If you use .select() before on/off, it will be effected only for the selected element.

Sometimes it is needed to off all events for some time. To do that use methods below.

// off click handlers
o(selector).offAll('click');
// off all event listeners
o(selector).offAll();
// on events
o(selector).onAll();

All inited events are saved in special parameter .ie - it saves each handler of each event to on/off them. You can get functions from there and init them on other elements.

// the first (0) click event function in array
objs.ie.click[0]
// contains:
// 0: function, 1: options or useCapture or undefined, 2: wantsUntrusted if was set or undefined

// copy click events to other elements with parameters
objs.ie.click.forEach(handler => {
	o(selector).on('click', ...handler);
});

By v2.0 were added functions for delegations and parents events. Event object has .o property with the current Objs object.

By onDelegate(event, selector, listener) event listener will be added to elements in Objs object and listener function will get event object with additional property .delegate with an element parent selected by el.closest(selector). E.g. parent tab element or other.

The onParent(event, selector, listener) adds listener on element, selected by querySelector(selector) and runs listener if event.target is one of elements.

// delegate
o(...).onDelegate('change, input', '.tab', listener);//
o(...).offDelegate(eventType);// removes all eventListeners of type

// adds listener on one parent node (by element or selector)
// and runs if Objs elements contain event.target
// e.g. one listener for all interactions
o(...).onParent('click', '.parentBlock', listener);
// removes listeners
o(...).offParent('.parentBlock', 'click');
Testing

State control for DOM elements

One of the features of Objs - states. It gets a special object of states collection and switches elements between them. It separates logic and veiw by using samples. Samples are bigger than components and allow to operate modules without micro separation.

Common states object

It is an object with states as object / string / function value.

If element has to be created - states object should contain render state for creation. Set tag with tag name there or it will be set as 'div'.

// common structure
const states = {
	// DEFAULT NAMED state, required for creating element
	render: {
		tag: "div",// tag name
		html: "string",// use `` to make big samples or append attribute
		class: "string",
		// it is possible to return nothing
		events: ({self}) => {
			self.on('click', handler);
		},
		// other attributes
	},
	// states to change element
	stateName: {
		attribute: "string",
		// function that returns attribute value
		class: ({disabled}) => {
			return `link ${disabled ? 'hidden' : ''}`;
		},
	},
	// state for on/off events
	startEventsName: ({self}) => {
		self.onAll('click');
	},
	pauseEventsName: ({self}) => {
		self.offAll('click');
	}
}

To append children elements, use append attribute in state with a DOM element or Objs objects or an array of them - they will be added in order.

// another example
const states = {
	render: (props) => {
		return `
			<h3>HTML ${props.title} here</h3>
			<p>Both tags will be in common DIV and 
			it is controlled by Objs, not these</p>
		`;
	},
	shown: {
		removeClass: 'hidden',
	},
	hidden: {
		addClass: 'hidden',
	}
}
				

If you need fast init and render, use .initState(state, data) - it creates or changes element for the state by rendering it immediately. It is equal to .init(state).render(data)

// creates element
const link = o.initState({
	tag: 'a', 
	href: '/', 
	innerText: 'Main page'
});
const activeState = {addClass: 'active'};

// fast change element
link.initState(activeState);

Creating elements

Depends on your architecture it is possible to use several types of states value. The simpliest is just string as state without state name. Recommended to use states object with states to be sure about elements initialisation and possibilities of control, e.g. .render() method can create an element for each data in array.

To create element use o.init(state).render() and then append it as a HTML or by special method. Without appending element will not appear in DOM.

// append as HTML
o('#root').innerHTML(o.init(states).render().html());

// append by method
o.init(states).render().appendInside('#root');

// set store property to connect data with component
const objs = o.init(state);
objs.store = {...data};

// append methods from Direct changing DOM elements section
objs.appendInside('#root');
objs.appendBefore(element);
objs.appendAfter('#child');

State string

// HTML string
o.init('<p>String</p>').render();

State as object.


o.init({// state
	tag: 'img',
	src: ({src, utm}) => {
		return src + '?utm=' + utm;
	},
	alt: ({alt}) => {return alt}
}).render(props);

State function returning string and .render(data) for 2 elements to create 2 of them and append in DOM.

// function that returns HTML string
o.init(({title, text, data, id}) => {
	return `
		<div id="article${id}" class="article">
			<h2>${title}</h2>
			<p>${text}<br>
			<hr>
			${data}</p>
		</div>
	`;
}).render([// automatically creates 2 elements for each data set
	{
		id: 1, 
		title: 'First title', 
		text: 'Text of an article', 
		data: '12/13/2022'
	},
	{
		id: 2, 
		title: 'Second title', 
		text: 'Text of an article', 
		data: '12/13/2022'
	},
]).appendInside('.o-articles');// inserts in a first ".o-articles" element

Recommended states object for creating and control elements.

Every state and attribute function gets self, o, i in parameters for inited object (self), o function and index of current element, e.g. for events to control special element, not all od them.


const banner = o.init({// states
	render: {
		tag: 'img',
		class: 'banner-img banner-hidden',
		src: ({src, utm}) => {
			return src + '?utm=' + utm;
		},
		alt: ({alt}) => {return alt}
	},
	shown: {
		removeClass: 'banner-hidden'
	},
	events: ({self}) => {
		document.addEventListener('scroll', () => {
			self.shown();
		})
	}
}).render(props).events();

Routing pages

In v2.0 added two global methods to make routing.

o.route(string|function|boolean, [state|callback]) gets string to compare with window.location.pathname, a function that should return true for route to be enabled, or a boolean (true to always match, false to never match). If no callback/state, o.route() will return o object for inline initialisation and methods usage. If callback is a function, it will be evaluated and return true. If it is a state object — it will be returned. Routing matches on pathname only; hash and query are not considered.

o.router() gets an object with paths and functions or states. So it runs function with equal path index or returns state object for current path for initialisation.

Use o.getParams([key]) to read GET (query) parameters from the URL. With no argument it returns an object of all query params; with a key string it returns that param's value. Useful inside route callbacks or when initialising components: pass o.getParams() to render(data), or read a specific key (e.g. o.getParams('id')) to drive component state or data loading.

// function route
o.route('/', initHomePage);

// inline route
o.route('/')?.init(HomePage).render().appendInside('#root');

// function for route verification
o.route((path) => path.startsWith('/item?id='))
	?.init(ItemPage)
	.render(itemData)
	.appendInside('#root');

// function for route verification and state return
o.init(
	o.route('/', HomePage)
	|| o.route((path) => path.startsWith('/item'), ItemPage)
).render(o.getParams() || data).appendInside('#root');  // pass query params into component

// inside a route callback or component: read a single param
const itemId = o.getParams('id');

// Simple router for small apps

// object for simple router with functions
o.router({
	'/': initHomePage,
	'/cart': initCartPage,
});

// object router as switcher between components
o.init(o.router({
	'/': HomePage,
	'/cart': CartPage,
})).render(data).appendInside('#root');
Testing

Switching states

To switch states of existing element you just select it by o(), load states with .init() and use methods named as states to switch elements between them easily. After every operation it returns Objs with all methods.

To change specific element use .select() before switching state. If you need to change child, use o(self).find(selector) to create separate object and control other elements.

// example
// creating and appending element
const menuID = o.init(menuStates)
					.render(menuData)
					.appendInside('#root')
					.initID;

// changing state somewhere
o(menuID).stiky();
// or use selector and special method
o.take('.class').stiky();

If element states were inited, it is cached and all states are available by o(query) or o(initID) without new initialization - example above.

Store loaders

By v2.0 a loader creation function added. It allows to request for data and connect components with it. If data is got, it runs the chosen state for component update.

Both loaders and Objs components have .connect() method.

// example
const pageLoader = o.newLoader(o.post(...));// returns Loader

// Loader object
{
	isObjsLoader: true,
	reload: (promise) => {},// method for reloading with new Promise
	connect: (listener, state='render', failState='') => {},// connect Objs component
	disconnect: (listener) => {},
	listeners: [],
	isFinished: () => {},// check if finished
	getStore: () => {},// get loaded data
}

// Init request
const pageLoader = o.newLoader(o.post(...));

// Connect component somewhere
o.init(states)
	.connect(pageLoader, 'show')
	.render()
	.appendInside('#root');

// Or connect loader
pageLoader.connect(Feed, 'update', 'internetError');

If element states were inited, it is cached and all states are available by o(query) or o(initID) without new initialization - example above.

Testing

Built-in store (o.createStore)

o.createStore(defaults) creates a reactive plain-object store. The returned object has subscribe(component, stateName), notify(), and reset(). Subscribed components receive store data merged into their state context on every notify(). No virtual DOM; only subscribed components update their own DOM (O(1) per subscriber). reset() restores original defaults.

const store = o.createStore({ count: 0 });
store.subscribe(myComponent, 'sync'); // myComponent.sync(storeData) on notify
store.count = 5;
store.notify();
store.reset(); // back to { count: 0 }
Testing

Example

Full example of real states object with render state and some other.
// example of states object
const states = {
	// state for creating elements if needed
	render: {
		tag: 'button',// default is 'div' (if doesn't set)
		class: 'button',// use addClass to add and removeClass to remove, 
		html: 'Click me',
		onclick: 'clickFunction()',
		disabled: true
	},
	// attaching events
	events: ({self, o}) => {
		// self is already initialized with all elements
		self.on('mouseenter', (e) => {
			o(e.target).active();// element is inited, so it has all states
		});
		self.on('mouseleave', (e) => {
			o(e.target).disabled();
		});
	},
	// other states and attributes to change
	special: {
		// use function to create any dynamic value or build inner HTML
		html: (props) => {return props.name},
		disabled: (props) => {return Boolean(props.disabled)}
	},
	error: {
		// there is o(), self, i in the props to control element
		showError: (props) => {
			// setting error text
			props.el.closest('.error-text').innerText = props.errorText;
		},
		disabled: true
	},
	active: {
		disabled: false,
		clearError: (props) => {
			props.el.closest('.error-text').innerText = '';
		}
	},
	disabled: {
		disabled: true,
	}
};

If state is a function, it gets props argument with o function, self (inited object), i (index of current element), and parent (ObjsInstance this was appendInside() into, or null).

If attribute is a function, it gets props argument (data is common with state props) with same o, self, i.

There are special attributes: addClass, removeClass, toggleClass to do so. Style attribute can be style: {} (object) to set more comfortable.

// inits existing element states and runs function for binding events
const btn = o('.button').init(states).events();
// making all ".button" elements "active" state
btn.active();
// switching state to "special" with some text
btn.special({name: 'Special state', disabled: false});

// creates an element from "init" state that can be appended in DOM
const newBtn = o.init(states).render().el;
// creates an element for each prop in props array
const newBtns = o.init(states).render([prop0, prop1]).el;

// it is possible to use state itself and default state will be render()
const newSameBtn = o.init({
	tag: 'button',
	class: 'button',
	html: 'Click me',
	onclick: 'clickFunction()',
	disabled: true
}).render().el;

// you can use just html to render blocks
const btnState = (props) => {
	return `<button class="button">${props.text}</button>`;
};
const anotherBtn = o.init(btnState).render({text: 'Button 1'});
// if props is an array, elements will be created for each
anotherBtn.reset().init(btnState).render([{text: 'Button 1'}, {text: 'Button 2'}]);

Creating states and HTML from DOM

// returns HTML of all elements, e.g. to make innerHTML
const classHTML = o('.class').html();
// returns HTML of all elements (in this case - just one created button)
const newBtnHTML = o.init(states).render().html();

// creating an object with "render" state from the first element
const newStates = o('.button').sample();
// creating an object from the second element
const newStates = o('.button').select(1).sample();

// example of using "cloning" state from DOM
o.initState({
	...newStates, 
	html: ({title}) => {return title},
	onclick: ({key}) => {globalSelect(key)},
},
[
	{title: 'Btn 1', key: 'one'},
	{title: 'Btn 2', key: 'two'},
]);

It is possible to get inited Objs object and DOM elements by o(initID) where initID is a parameter objs.initID. To store an instance by a string name, use .saveAs(key) — the instance is then available as o.getSaved[key]. Keys are unique; if the key already exists, saveAs does not overwrite (in debug mode a warning is logged).

It gets only inited elements from Objs object, not from DOM tree.

From v2.0 there are additional functions for camel case and kebab case transformation between attributes and variables. Also there are reducers for global data.

// examples
o.kebabToCamel(string);// o-init to oInit
o.camelToKebab(string);// oInit to o-init

// get array from all inited objects by initId as index
o.getStates();
o.getStores();
o.getListeners();// gets onDelegate() listeners too

// save/restore DOM state of an ObjsInstance (for undo or compare)
const comp = o.init(states).render().appendInside('#root');
comp.saveState('initial');   // save current DOM state with id 'initial'
comp.update('New content');  // change element
comp.revertState('initial'); // restore to saved state
comp.loseState('initial');   // discard saved state from memory

// save ObjsInstance by name — store in o.getSaved{} for later access by key
const menu = o.init(menuStates).render().appendInside('#nav');
menu.saveAs('mainMenu');     // saves this instance as o.getSaved['mainMenu']
// elsewhere: get the instance by name
o.getSaved['mainMenu'].updateItems(data);
o.getSaved.mainMenu;         // same; key must be non-empty string. Does not overwrite if key exists (debug warns).
Testing

Server side rendering

In v2.0 added SSR. On server side there is no interactivity so some methods won't affect on elements. In Node, o.D is o.DocumentMVP (no real DOM). To initiate events and manipulate server-rendered elements in the browser, use getSSR(initId) after init(). Call .html() with no arguments on the result of render() to get the element(s) HTML string — in Node this uses DocumentMVP serialization, so you can verify or serialize output without a browser.

// by getSSR() Objs gets rendered elements on page by initID
o.init(states).getSSR().events();

// initID property is available after init() method and can be used in getSSR() as a parameter
o.init(states).getSSR(32).events();// Objs object will overwrite o.inits[32]

// get HTML string (browser or Node) for verification or SSR output
const htmlString = o.init(states).render().html();

On server side getSSR() method does nothing, so you can use the same code on both sides.

Testing

JSX-like form with auto-hydration

Parent is a <form>; children are design-system atoms in separate blocks (form__header, form__body, form__list, form__actions). When the form sets innerHTML with markup that contains data-o-init, Objs auto-hydrates those elements so inited instances are bound to the new DOM nodes.

Example source

// Email validation helper
const emailValid = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((v || '').trim());

// Atom states: each renders one element (or list items); events reattach on auto-hydration
const FormHeaderStates = {
  render: (p) => ({ tag: 'h2', className: 'form-atom form-atom__header', html: p.title }),
};
const FormFieldStates = {
  render: ({ self, ...p }) => ({
    tag: 'div', className: 'form-atom form-atom__field',
    html: `<label>${p.label}</label><input ref="input" type="${p.type || 'text'}" name="${p.name}"><span ref="error" class="form-atom__field-error"></span>`,
    events: {
      blur: {
        targetRef: 'input',
        handler: (e) => {
          const row = self.select(e);
          if (!row.refs?.error) return;
          const value = row.refs.input.val();
          const valid = emailValid(value);
          if (!valid && value.trim()) {
            row.refs.error.html('Invalid email');
            row.el?.classList.add('form-atom__field--error');
          } else {
            row.refs.error.html(''); row.el?.classList.remove('form-atom__field--error');
          }
        },
      },
    },
  }),
};
const FormItemListStates = {
  render: (p) => ({ tag: 'li', className: 'form-atom__list-item', html: p.title }),
};
const FormButtonStates = {
  render: (p) => ({
    tag: 'button', type: 'submit', className: 'form-atom form-atom__btn', html: p.label,
  }),
};
// Parent form: inits children, builds HTML from blocks; success informer near button
const FormStates = {
  render: ({ self, data }) => {
    const header = o.init(FormHeaderStates).render([{ title: data.title }]);
    const field = o.init(FormFieldStates).render([{ label: data.emailLabel, name: 'email', type: 'email' }]);
    const list = o.init(FormItemListStates).render(data.items);
    const btn = o.init(FormButtonStates).render([{ label: data.submitLabel }]);
    self.store = { header, field, list, btn };
    const informer = `<div ref="successInformer" class="form-informer form-informer--success" style="display:none;"></div>`;
    const html = `<div class="form__header">${header.html()}</div><div class="form__body">${field.html()}</div><div class="form__list"><ul class="form-atom form-atom__list">${list.html()}</ul></div><div class="form__actions">${btn.html()}${informer}</div>`;
    return { tag: 'form', className: 'form', html,
      events: { submit: (e) => { e.preventDefault(); self.submit?.(self); } } };
  },
  submit: (self) => {
    const field = self?.store?.field;
    const value = field?.refs?.input?.val();
    const valid = value !== undefined && emailValid(value);
    const errRef = field?.refs?.error;
    if (!valid) {
      if (errRef) errRef.html('Please enter a valid email');
      if (field?.el) field.el.classList.add('form-atom__field--error');
      return;
    }
    if (errRef) errRef.html(''); if (field?.el) field.el.classList.remove('form-atom__field--error');
    const inf = self.refs?.successInformer;
    if (inf) { inf.html('Success!'); inf.css({ display: 'block' }); }
  },
};
// Mount: append then getSSR so form submit and refs bind to the in-document element
const form = o.init(FormStates).render([formData]);
form.appendInside('#testJSXForm');
form.getSSR(form.initID);

Load and cache scripts, styles, images preload

There is a function o.inc() to import JS scripts / CSS styles / images for modules and HTML states. Also it makes cache in localStorage by sending GET request (enabled by default).

In v2.0 added hash functionality. If hash in loading url (e.g. script.js?20250225) differs from previous value, file will overwrite cached version. To load without appending scripts/styles to the DOM, add a key "preload" (truthy value) to the sources object: o.inc({ preload: true, modal: 'modal.js' }, callback) — resources are fetched and cached but not injected into the page.

// parameters and defaults
// set false to disable
o.incCache = true;
// 24 hours to store cache
o.incCacheExp = 1000 * 60 * 60 * 24;
// timeOut to load all functions. After - cancels callback and runs failure function
o.incTimeOut = 6000;
// addition for src in the beginning
o.incSource = '';
// prevents repeated including with the same ID (if included before)
o.incForce = false;
// set to false if you need an order in loading scripts
o.incAsync = true;
// change standart hash separator if needed
o.incSeparator = '?';
// change hash getting function if needed, standart gets hash like "script.js?hash"
o.incGetHash = (path) => path.split(o.incSeparator)[1] || '';

// service data
// array of all "name: 1/0" statuses of included scripts to check directly
o.incFns = {};
// array of including sets statuses
o.incSet = [0, ...];

Here is the main function. It returns ID of the loading set from o.incSet[] where is 1 for successful load of all scripts and 0 for failure. If scripts are still loading now - there is a callback function. If sources is missing - it returns id of the last loading.

const incId = o.inc({// returns ID to check the status
	'name': 'src'// name and src to JS, CSS or an image file
}, callback, callbad);

// simple version with array of links
o.inc([
	'modal.js',
	'modal.css',
	'modalBackground.jpg',
], (setId) => {
	// success function gets set ID, e.g. to check statuses
});

o.incSource = 'scripts/';
o.inc({
	pow: 'pow.js'// name for identification and src for loading script
	powStyle: 'pow.css'// styles for the module
	banner: 'pow.jpg'// image to preload
}, () => {// function to run after loading
	console.log('Successful using script with pow()');
}, () => {// function to run if failed with timeout
	console.log('Loading failed');
});

// checks last loading set of functions
if (o.incCheck(o.inc())) { ... }
// checks special set by ID
if (o.incCheck(incId)) { ... }
// check if the current script is loaded, 'pow' - script name
if (o.incFns['pow']) { ... }

// clears all localStorage cache
// if "all" is true - clears all system parameters
o.incCacheClear(all);

If o.inc() gets an array, there will not be any cache control or check for repeat. Also there is no cache for links started with 'http'. With o.incCors false (default), cross-origin URLs may fail unless same-origin or server allows.

Testing

GET and POST requests

There are some basic promise functions for sending requests. It's possible to use just one parameter - url. They return Promise so you can use .then() or await.

// simple GET
o.get(url)
	.then();
// GET with object data and JSON response
o.get(url, {
	data: {
		param1: 'value1',
		// object value will be JSON stringified
		param2: {
			...
		}
	}
})
	.then(response => response.json())
	.then((response) => {
		...
	}); 

For POST you can use body parameter for special data or data parameter for auto-creation data-string.

// POST with object data and JSON response
o.post(url, {
	data: {
		param1: 'value1',
		// object value will be JSON stringified
		param2: {
			...
		}
	}
})
	.then(response => response.json())
	.then((response) => {
		...
	});

To unify request use .ajax() - it can be GET or POST request.

o.ajax(url, {
	method: 'post',// should be post or get
	// other parameters, e.g. data
})
	.then(response => response.json())
	.then((response) => {
		...
	});

Use o.getParams() to have an array of GET parameters of the page

const params = o.getParams();
Testing

Unit tests and its parameters

Test function is unified and gets test title and tests and as the last parameter can be finish function. It runs after tests or by timeout. Results log can be in two kinds: console style (default) and HTML (o.tStyles = true) – both you find below in test section.

Test expression or function should return true for successful check. If it is false or some string - it marks as error and string is shown as an error text.

To test async functions e.g. requests - use o.testUpdate() with params in check function.

// parameters and defaults
// set to true to see success test results too
o.tShowOk = false;
// set to true to make results HTML styled
o.tStyled = false;
// when tStyled is true, customize HTML for log output: o.tPre, o.tOk, o.tXx, o.tDc
// timeout of async tests
o.tTime = 2000;
// set to true for auto consol log
o.tAutolog = false;

// an array of all test sessions
// get session you want and check results or .join() to see all of them
o.tLog = [];
// an array of true/false results of all test sessions
o.tRes = [];
// an array of arrays with true/false results of each test in sessions
o.tStatus = [];

// usage
o.test("session / function title",// the main title of this test session
	// array with a title of check and expression/function to be true
	["test/check title", func() === val],
	["test/check title", () => {
		// return true for success and false or error text for failure
		return true;
	}],
	["title for tests division"],// logs test divider
	// get info for async tests
	["test/check title", (info) => {
		// use o.testUpdate to update test status
		setTimeout(() => {
			o.testUpdate(info, true, ' - some additional text');
		}, 100);
	}],
	// optional last argument: completion callback (test id); runs when all tests finish or on timeout
	(testN) => {
		console.log('Result: \n' + o.tLog[testN]);
	}
);

// o.runTest(testId, autoRun, savePrev) — savePrev: when true, keeps existing sessionStorage for that testId so the run can resume

There is a function that will be called on errors during Objs functions. Set o.onError to a function and display or log errors as you need. As default, errors will not be shown.

o.onError = (e, name) => {// function for errors
	if (o.showErrors) {
		console.error(e, name);
	} else {
		o.errors.push(e);
		if (name) {
			o.errors.push(name);
		}
	}
};

Type verification (o.verify, o.specialTypes)

o.verify(pairs, safe?) and o.safeVerify(pairs) give you runtime type checking for function arguments, config objects, or API responses. They are useful in projects to fail fast at API boundaries, validate options before use, and keep code (including AI-generated code) predictable. Objs uses the same o.verify and o.specialTypes internally, so your validators are part of one shared system.

pairs is an array of [value, expectedTypes]. expectedTypes is a string or array of strings: built-in typeof names ("number", "string", "boolean", "object", "function", "undefined") or keys from o.specialTypes. Pairs are checked in order; o.verify returns true when the first pair matches (remaining pairs are not checked). If no pair matches, it throws (or returns an Error when safe is true); o.safeVerify returns false.

Developers can add global validators by extending o.specialTypes. Assign a function (value, typeofValue) => boolean to o.specialTypes.myType. That validator is then available everywhere—in your app and inside Objs—so you can use o.verify([x, ['myType']]) consistently.

// validate function arguments (throws on failure)
const method = (index, newValues) => {
	o.verify([
		[index, ["number", "string"]],
		[newValues, ["array", "undefined"]],
	]);
	// function code...
};

// check without throwing
if (o.safeVerify([[id, ["number"]]])) {
	// code...
}

// built-in special types: "array" (Array.isArray), "notEmptyString" (string with length), "promise"

// add a global validator — use it anywhere in the project
o.specialTypes.user = (val, type) => {
	return type === 'object' && val !== null && typeof val.id === 'number' && typeof val.login === 'string';
};
o.verify([[user, ["user"]]]);
Testing

Examples for each test:

tStyles: false; (console view but \n changed to br tags)

tStyles: true; (HTML)

Tests with page reload

With v2.0 it is possible to test with page reload, e.g. for registration tests or purchases. Test will continue after page reload and save results. Also the auto-run is added to make complete test sessions.

To set test with reload, use o.addTest() and split test logic in two. The first part is test for preparation and reload. The second - to continue the verification.

Use o.clearCookies(), o.clearLocalStorage() and o.clearSessionStorage() functions to start next test with autorun without effects from previous.

// add test with reload to global tests list
const testReloadable = o.addTest('Reload tests',
	['Start test with reload: preparation and reload...', (t) => {
		// some preparation...
		// switch current test to success
		o.testUpdate(t, true);
		window.location = window.location;// do some reload action
	}],
	['End test after reload: check results', () => {
		// check the behaviour after reload
		return Boolean(o.getCookie('authID'));
	}],
	(testId) => {
		console.log(o.tLog[testId]);
	}
);

// run test
testReloadable.run();
// run this and autorun next tests
testReloadable.autorun();
// also test ID is available
o.runTest(testReloadable.testId);

// load test results from sessionStorage
o.updateLogs();

Results in sessionStorage are saved only for tests added by o.addTest() after run. Continue after reload and autorun are supported for them only too. On file:// or restricted environments, cookies may be in-memory only.

Testing

Cookies and LS / SS

In v2.0 added functions for Cookies management and localStorage/sessionStorage clearing.

To have your own error tool, set o.onError to the function you need. To see hidden errors – get them from o.errors array.

o.setCookie(title, value, [params]);
o.getCookie(title);
o.deleteCookie(title);

// clearing functions, e.g. for tests
o.clearCookies();
o.clearLocalStorage([all]);// clears all storage, except Objs data if all !== true
o.clearSessionStorage();// clears all sessionStorage
o.clearTestsStorage();// clears tests data from sessionStorage
o.clearAfterTests();// clear cookies and test-related storage after test run (e.g. in tAfterEach)
Testing

User actions recording

o.startRecording(observe?, events?, timeouts?) — starts capturing user interactions and network requests. Optional observe is a CSS selector to scope the MutationObserver. Check o.recorder.active to see if recording is on. o.stopRecording() returns { actions, mocks, initialData, assertions, observeRoot, stepDelays }. stepDelays is an optional per-event delay map used when replaying (from timeouts in startRecording). o.exportTest(recording) returns Objs-style o.addTest() source. o.exportPlaywrightTest(recording, options) returns a Playwright .spec.ts with locators and mocks. o.clearRecording([id]) removes from sessionStorage. o.playRecording(recording) replays as a test (all builds). Use o.testOverlay() so assessors can see auto test results and which manual checks failed.

Security: o.startRecording() intercepts window.fetch and captures request/response bodies (including auth tokens). Appropriate for staging; review before enabling on production.

Recording API

Testing

Export to Objs (exportTest)

Testing

Export to Playwright

Testing

Recording fixture and replay

Test fixture (elements used by the tests):

Test results:

Auto tests and overlay (testOverlay, testConfirm)

o.testOverlay() — Renders a fixed overlay button (🧪 Tests). Click to see pass/fail for all test runs. For assessors: after replay, open the overlay to see if all auto tests passed and which manual checks failed. Available in all builds (prod and staging). o.testConfirm(label, items?, opts?) — Shows a draggable overlay with optional checklist; returns Promise<{ ok, errors? }>. Use after replay for manual checks (e.g. hover effects). Available in all builds. o.measure(el), o.assertVisible(el), o.assertSize(el, expected) — layout measurement and assertions for use inside o.test() (design system / UI verification).

Testing

Debug settings

Error messages are off by default, but you can turn them on by changing o.showErrors to true. Or use o.logErrors() to see all hidden errors in the console.

To have your own error tool, set o.onError to the function you need. To see hidden errors – get them from o.errors array.

o.errors.forEach(o.onError)

Restrictions

States should NOT be like Objs methods or parameters below

length
el
els
last
ie
initID
initedEvents

Params for states should NOT have keys like o, i, self.

Events should be added by self parameter in states to be in ie array of controlled listeners.

States are overwritable - they can be over write or over write parameters. Be careful.

Corner cases and limitations

  • State arguments: Passing a primitive (e.g. comp.setState(5)) — use data in the state context. Passing an object spreads its keys into the context.
  • o.take(q): Returns inited components only when the number of matching DOM elements equals the number of previously inited components; otherwise it behaves like o(q).
  • attr / style: attr(name, null) removes the attribute; attr(name, '') sets it to empty string. style(null) or css(null) removes the style attribute entirely.
  • val(): Intended for input, textarea, select; behavior on other elements is undefined.
  • add(): Works for already-got (existing) DOM elements; not for creating new component instances.
  • getSSR: In Node, getSSR() does nothing (no real DOM). In the browser, getSSR(initId) hydrates from existing DOM by initID.
  • Recording replay: o.playRecording() depends on selectors and list indices; dynamic content that changes order can make replays flaky.
  • o.inc: With o.incCors false (default), cross-origin URLs may fail unless same-origin or server allows. In environments without localStorage (e.g. Node), o.inc() returns without loading.
  • add(): On an inited component (with initID), add() does nothing. add(number) with a valid init index returns that inited component (like o(number)). When appending into an ObjsInstance, the appended instance's _parent is set to that target.
  • Storage keys: Objs uses oInc-* (localStorage for inc cache) and oTest-* (sessionStorage for tests). clearLocalStorage(false) leaves keys containing oInc- or oTest-; clearSessionStorage(true) removes only keys starting with oTest-. Avoid these prefixes in app code.
  • Node vs browser: In Node, o.D is DocumentMVP; no window, no real document, no events. Methods that rely on DOM (e.g. events, focus) have no effect.
  • Tests with reload: Reload-based tests use sessionStorage and optional cookies; on file:// or restricted environments, cookies may be in-memory only.
  • Utilities: o.C(obj, key) is a safe hasOwn check. o.F, o.U are reserved for internal use. o.W, o.H exist but are unused; do not rely on them.
Go top