UniRender is a compact JavaScript library that provides full rendering and business logic for web and mobile applications using a special config in JSON format.
This approach significantly simplifies project development (especially complex ones) by removing two layers:
Currently, the VUE framework is supported for rendering, with components from the PrimeVUE package. Custom components can be added as needed.
The modular architecture of the project allows for easy addition of new components and new frameworks (React, React Native, etc.). Contributors are welcome to participate in the project’s development.
window.uni.switch()
Upon the first visit to the site, index.html
, which is only 400 bytes in size, is loaded. This file loads the unirender.js
library and specifies the unified API URL for backend interaction.
UniRender immediately sends a request to the API URL, along with referer, user-agent, and other session attributes.
In response, the backend sends a UniRender config, containing a flat list of all elements, their structural placement, attribute values, and necessary methods for events that are handled on that page.
UniRender processes the received UniRender config, unpacks keys into Storage, and initiates rendering using VUE (or another framework). Important: all components must be reactive.
When an event is triggered, UniRender executes the specified JavaScript code for the event, or it executes the UniRender function window.uni.switch()
, which collects the necessary keys from Storage and calls the API URL.
In response, the backend sends a new UniRender config, containing keys only for the elements that need to be updated (or added), along with new attribute values. Typically, this involves only a few dozen elements.
UniRender processes the received UniRender config and unpacks the keys into Storage. Since all components are reactive, the data is immediately updated on the web page.
Thus, all project business logic is moved to the backend. This is known as the Backend-Driven approach.
Removes two complex layers: frontend business logic + API layer.
A detailed project description transforms into a set of short prompts, describing specific events – user actions. This improves the quality and speed of each step: the LLM is not required to flawlessly solve a large task, and the user does not need to rewrite the prompt 10 times.
Detailed context for the entire project in a language understandable by LLMs:
You can embed UniRender forms into any existing landing page / website without overhauling the entire site. Simply load unirender.js
into a <div>
container and set the correct apiHost
.
This allows expanding existing products created with No Code / Low Code builders, adding business logic of any complexity.
Seamless integration of UI templates with backend business logic. A template is simply added to the form – and interaction with the backend is ready. Deep integration with existing functions can be achieved with AI, as there is full context on the component and data structure.
In the classical approach, implementing complex templates requires integration on the frontend (SDK), at the API level (with documentation updates), and at the backend level. Updates to such templates are fraught with risks of consistency violations.
The goal is to make the interface as convenient as possible for a specific user.
In a backend-driven solution, not only text but also the set of components, their arrangement, colors, styles, and any other attributes can be easily changed. The customization logic can be determined with the help of AI.
Any page on a site is critical. And any of them can be optimized not manually, but with AI. And any of them can and should be optimized not generally – but adapted to each visitor, taking into account their profile, where they came to the site from now, and the history of their past visits.
Automatic generation of multi-level admin panels from data structure descriptions. Flexible visualization settings at both the individual field level and the field set level.
A Product Manager gets a full-fledged CMS already at the project development stage – each new entity and field is automatically ready for population.
A JavaScript component of the corresponding framework (VUE / React / etc):
An instance of a component within a specific View:
attributes.*
, options.*
, store.*
, and methods.*
values.One or more related elements:
head[]
with styles and scripts.<div>
container of any website.A logically complete construct consisting of one or more View(s). A project can have any number of Pages.
A JSON object that describes all elements and their attributes and methods, as well as auxiliary data and the URL for display in the address bar.
A JSON object in browser storage that stores Element settings (attributes
, options
, methods
), dynamic data (input fields, table cells, etc.), and auxiliary data.
The JavaScript library for the following tasks:
.umd
format."axios": "^1.6.0"
"jsonpath-plus": "^7.2.0"
"lodash": "^4.17.21"
index.html
.That’s all! Now your frontend is ready for work.
$ npm i unirender
$ yarn add unirender
For Vue add following to main.js:
import { UniRender, UniUrl, apiHost } from "unirender-npm-package";
const initConfig = {
store: Vue.reactive(storageData["store"] || {}),
service: Vue.reactive(storageData["service"] || {}),
components: Vue.reactive(storageData["components"] || {}),
composition: Vue.reactive(storageData["composition"] || {}),
methods: Vue.reactive(storageData["methods"] || {}),
};
uniRender = new UniRender(initConfig);
uniRender.services.hookService
.subscribe("error", [
(error) => {
console.error(error);
},
])
.subscribe("component.load", [
({ loader, name }) => {
uniRender.services.componentService.putCompiledComponent(
name,
Vue.defineAsyncComponent(() => loader())
);
},
]);
await uniRender.start();
Run UniRender config builder: https://dev.unibackend.com/config
View UniRender config
You can make changes in the UniRender config to change layout in the right part of the screen.
Demo with interactive components: https://dev.unibackend.com/demo
View UniRender config
Click buttons to change elements layout and attributes.
Multiplayer word battles to practise 42 foreign languages: https://tokling.com
View UniRender config
You can check http requests and responses in the Developer Console to understand the UniRender principles better. Pay attention, that all requests in all the projects have the same format.
And all the projects have the same Page Source.
TBD
TBD
"axios": "^1.6.0"
,"jsonpath-plus": "^7.2.0"
,"lodash": "^4.17.21"
index.html
.That’s all! Now your frontend is ready for work.
API request parameters from window.uni.switch()
`:
url
:
{
"protocol": "https", // optional
"host": "<my_domain.org>", // optional
"path": "/path/to/part",
"parameters": "params", // string, optional
"anchor": "#myhash", // string, optional
"tab_id": "active browser tab ID"
}
query
: { <required keys and values from [Storage](#structure-of-storage), event, and props> }
Example:
{
"query": {
"components": {
"element_1": {
"attributes": {
"label": "Text 1",
"class": ["class_1"]
}
}
},
"store": {
"element_2": {
"input": "New text"
}
}
}
}
uniData
:
{
"device": "desktop | tablet | mobile", // user's device
"window": {
"width": 800, // resolution: width
"height": 600 // resolution: height
}
}
The response includes the UniRender config, which may contain all or part of the following sections:
composition
: An object specifying the list of top-level elements:
{
"point": "<name of the topmost component>",
"title": "",
"css": [],
"js": [],
"config_url": "<optional: .json file with [UniRender config](#unirender-config-format-detailed)>"
}
components
: A flat list of all elements on the page. All container elements specify which elements are included in them. The nesting level is unlimited.store
: An object with dynamic keys; each elements can have from 0 to several such keys. For example, a button might not have such a key, but a table has a key with an array of data (objects), a key for storing a list of selected records, a key with sort statuses, etc.methods
: An object with methods that handle the necessary elements events. Typically, this is a call to the window.uni.switch()
function.service
: An object with auxiliary keys; these keys are not processed on the frontend.url
: An object with the URL of the current page; this URL can include various parameters:
{
"protocol": "https", // optional
"host": "<my_domain.org>", // optional
"path": "/path/to/part",
"parameters": "params", // string, optional
"anchor": "#myhash", // string, optional
"target": "blank", // string, optional
"tab_id": "active browser tab ID"
}
actions
: [{<action to run>}]
(Array of action objects.)actions
.window.uni.switch()
function or a JavaScript script.switch()
function is called, it forms and sends a request to the backend. Upon receiving a response, it proceeds to step 2.The Storage JSON object has the following sections:
"components"
: Stores the settings for all elements. For each element, it specifies which keys in "store"
and "methods"
it is linked to."methods"
: A list of methods with JavaScript code. Typically, this is a call to the window.uni.switch()
function."params"
: {"initUrl", "apiHost"}
"store"
: Dynamic data that changes with the element (input field content, selected records, etc.)."service"
: An object with auxiliary data. A checksum is also saved for consistency verification."config"
: The current config from the server, excluding store
and service
.window.uni.switch()
For handling methods (e.g., a button click), the window.uni.switch()
function is used, through which all elements can interact with the backend.
Parameters for calling window.uni.switch()
from an element:
event
: Event object (mandatory).props
: Props passed to the element from which the event was called (mandatory).uniKeys
: A list of keys from Storage, event, and props, whose values need to be sent in the request to the backend. Regex can be used (optional).
"components.my_element_1.attributes.label"
should be passed as an array of arrays:
[
["exact", "components"],
["exact", "action_add_elements"],
["exact", "attributes"],
["exact", "label"]
]
"input_form_"
:
[
["exact", "store"],
["regex", "^input_form_.*"]
]
js
: JavaScript code to execute on the frontend (optional). If specified, no API call occurs.js-hooks
: afterApply
, beforeApply
(op[tional). This is JavaScript code that will run before and after the window.uni.switch()
call.url
: Where to navigate (optional). Object format:
{
"protocol": "https", // optional
"host": "<my_domain.org>", // optional
"path": "/path/to/part",
"parameters": "params", // string, optional
"anchor": "#myhash", // string, optional
"target": "blank", // string, optional
"tab_id": "active browser tab ID"
}
An array of objects describing actions. The rules are as follows:
"pauseBefore": <msec>
.List of actions:
delete
Deletes all keys specified in the key[]
array, from the section: "store"
| "components"
| "methods"
| "service"
. Regular expressions are supported for key searching.
Example:
{
"action": "delete",
"when": "afterApply" | "beforeApply",
"key": [
[["exact", "components"], ["regex", "^filter_.*"]],
[["exact", "store"], ["exact", "key_01"]]
]
}
Result:
element_key
starts with “filter_” are deleted.element_key
= “key_01” is deleted.update
(or alias jsonpath_update
)Updates keys in an array based on a JSONPath condition.
Example:
{
"action": "update",
"type": "replace" | "merge" | "add",
"when": "afterApply" (default) | "beforeApply",
"path": "$.store.my_table[?(@id===2)]",
"data": { "label": "new label", "desc": "new desc" }
}
type
options:
replace
: Replace one object/array/any type with another.merge
: Merge objects, replace non-object types.add
: Expand arrays (not applicable for other types).call
Executes any method. All method parameters are passed at the top level.
Example:
{
"action": "call",
"method": "goto",
"path": "/admin",
"pauseBefore": 3000
}
localStorage
Saves the specified keys to the client’s localStorage
. Subsequently, values saved in localStorage
will be sent in the initial request (e.g., when the page refreshes).
Example:
{
"action": "localStorage",
"key": [
[["exact", "components"], ["regex", "^filter_.*"]],
[["exact", "store"], ["exact", "key_01"]]
]
}
config
Loads UniRender config from a file. Convenient for quickly displaying a web page.
Example:
{
"action": "config",
"url": "https://static-dev.tokling.com/config.json",
"when": "beforeApply"
}
trigger
Executes a method on a specific element.
Example:
{
"action": "trigger",
"event": "click",
"key": [
["exact", "key_01"]
],
"uniKeys": [[...], [...]],
"when": "afterApply"
}
seqApply
Applies the UniRender config step-by-step, with timeline support. This allows for demonstrations (showing order of actions) and animations (rapid changes in colors / element sizes, hiding / showing, gradual changes in values, etc.).
Example:
{
"action": "seqApply",
"when": "afterApply",
"id": "seq_id_1",
"data": [
{
"wait": "<milliseconds since last event>",
"config": {"components": {}, "store": {}}
}
],
"cycles": "<how many times to repeat the whole cycle, default 1>",
"flag": ["stop" = "stop seqApply execution"]
}
Ability to embed UniRender into any website:
<div id="unirender1"></div>
<div id="unirender2"></div>
UniRender config will be rendered in these containers.
How to connect UniRender:
<head>
: <script src="<URL to load unirender.js>"></script>
<script>(new Unirender({...})).mount('#unirender1')</script>
{
"components": {
"element_1": {},
"element_2": {}
},
"composition": {
"point": {
"#div1": "element_1",
"#div2": "element_2"
}
}
}
This provides the following capabilities:
components
(JSON-object)Contains all elements necessary for rendering. Each element has a unique name, consisting only of Latin letters, numbers, and the symbols _
, -
, ~
, #
.
The element’s name is the key of the components
JSON object.
Properties of each element:
key
(string): Unique name of the element. Must match the component’s key in components
(mandatory).url
(string): Full link to the .umd
file of the component (mandatory).attributes
(object): Object with the element’s main attributes, their names strictly defined in the base component (e.g., label
, style
). Only properties passed to the original component are listed here (mandatory).options
(object): Object with additional element attributes. These properties are processed in the Component wrapper (optional).methods
(object): Element methods, their names strictly defined in the component (e.g., click
, focus
, scroll
) (optional).components
(array): Array of components that will be created inside this container component (optional).store
(object): Component’s store. Each component can have only one store (mandatory).css
(array): CSS files for styling this component (optional).composition
(JSON-object)The entry point to the application.
Properties of composition
:
point
(string): Indicates the name of the main (parent) element.title
(string): Page title.head
: (array of objects)
type
(string): Can contain the following values: script
, style
, link
, meta
.innerHTML
(string): For specifying content inside the tag.id
(string): Identifier for the script, style, or meta-data.attributes
(object): Attributes specific to a particular head tag according to specification.In head
are loaded page settings. Static data for all project pages can be loaded in index.html
.
Example:
[
{"type": "script", "src": "https://….js"},
{"type": "css", "innerHTML": ".container { font-size: 12px; }"}
]
methods
(JSON-object)A call to window.uni.switch()
or any JavaScript code that is executed by this method.
The key of the methods
object is the method name, as specified in “methods” when describing the element (e.g., "element_1-click"
).
The property of the methods
object is executable JavaScript code.
When calling the function window.uni.switch()
, the following parameters can be passed:
uniKeys
: A list of keys from Storage, event, and props, whose values need to be sent in the request to the backend. Regex can be used (optional).
"components.my_element_1.attributes.label"
should be passed as an array of arrays:
[
["exact", "components"],
["exact", "action_add_elements"],
["exact", "attributes"],
["exact", "label"]
]
"input_form_"
:
[
["exact", "store"],
["regex", "^input_form_.*"]
]
js-hooks
: beforeApply
, afterApply
. This is JavaScript code that will run before and after the window.uni.switch()
call.Example:
(event) => { window.uni.switch({ apiUrl:'/ub/render', uniKeys: [[["exact", "components"], ["exact", "action_add_elements"], ["exact", "attributes"], ["exact", "label"]]], ...event }); }
store
(JSON-object)Properties of store
:
All components refer to this. Property names are the same as the store
value of the element.
Example store
reference within an element:
{
"input_text_1": {
"store": "input_text_1"
}
}
Example global store
settings:
{
"store": {
"input_text_1": {
"input": "foo bar"
}
}
}
A special universal wrapper over any component, providing everything necessary for its connection.
A wrapper is (usually) a renderless component that has no layout but simply passes everything necessary for its operation to the nested component.
getComponent
and callMethod
).componentKey
, modelValue
).config.store
and passes it to the component via slotProps
as data
.config.attributes
and config.methods
to the component.slotProps
are passed to any component!
Data
: Component’s store.Config
: Component’s config.Props
: Component’s props.Attrs
: Component’s attributes (config.attributes
).Options
: Component’s options (config.options
).Events
: Component’s methods (config.methods
).Components
: Components nested within this component (config.components
).getComponent
: Function to create a component from the config.callMethod
: Function to create a method from the config.fallthroughAttrs
: Attributes that are automatically assigned to the inner component (currently only componentKey
).Each element has only one store. Anything can be created inside it. An element must refer to its store – typically, this is an object within Storage.store
, with a key matching the element’s name.
From the wrapper, the store arrives at the component as a reactive data
object. From there, you can take what is needed: data.input
, data.selected
, etc.
A component can take data from both props.modelValue
and store
, so logic for both cases must be written inside the component. Props also come from the wrapper.
The wrapper accepts standard props for correct operation and passes them to nested components.
const propsList = {
componentKey: String,
modelValue: String,
};
componentKey
: Component name.modelValue
: Component value when working via store.An element may have many methods. They are described in UniRender-config, inside the "components"
object:
{
"components": {
"button_1": {
"methods": {
"click": "button_1_update"
}
}
}
}
The method’s behavior is described in UniRender-config, in the "methods"
object:
{
"methods": {
"button_1_update": "(event) => { console.error(event)}"
}
}
All methods will be automatically passed to the component by the wrapper.