# pardnchiu/QuickUI > QuickUI is a zero-dependency virtual DOM frontend framework in vanilla JavaScript with no build step, by 邱敬幃 Pardn Chiu (pardnchiu). Every documentation page in navigation order; each section starts with its source URL. --- Source: https://quickui.pardn.io/index.md > [!NOTE] > This README was generated by [SKILL](https://github.com/agenvoy/skill-readme-generate), get the ZH version from [here](https://github.com/pardnchiu/QuickUI/blob/main/doc/README.zh.md). ***

A ZERO-DEPENDENCY VIRTUAL DOM FRAMEWORK THAT RUNS WITHOUT A BUILD STEP

npm Downloads License

*** > A vanilla JavaScript frontend framework with virtual DOM diffing, declarative HTML templates, and runtime i18n ## Table of Contents - [Features](#features) - [Architecture](#architecture) - [License](#license) - [Author](#author) ## Features > `npm i @pardnchiu/quickui` · [Documentation](https://github.com/pardnchiu/QuickUI/blob/main/doc/doc.md) - **Zero dependencies, no build step** — Native browser APIs power the virtual DOM diff/patch engine, so a single script tag brings it to any existing page. - **Declarative HTML templates** — Interpolation, loops, conditionals, two-way binding, and event binding live directly in HTML attributes, so the template is the page itself. - **Batched Proxy reactivity** — Deep data mutations trigger a 300ms debounced update that applies only the required DOM patches on the next animation frame. - **Runtime i18n switching** — Locales accept either inline objects or JSON file URLs, and a single language switch re-renders both text and attributes. - **Built-in lazy images and inline SVG** — IntersectionObserver defers image loading behind a placeholder animation and inlines external SVG files as stylable nodes once they enter the viewport. ## Architecture > [Full Architecture](https://github.com/pardnchiu/QuickUI/blob/main/doc/architecture.md) ```mermaid graph TB A[HTML Template / render Function] --> B[vDOM Build] D[Proxy Reactive Data] -->|Change, 300ms Debounce| B E[i18n Locale Data] --> B B --> C[Diff to Patches] C -->|requestAnimationFrame| F[Real DOM] G[Lifecycle Hooks] -.-> B F --> H[Lazyload / SVG Listeners] ``` ## License This project is licensed under the [MIT LICENSE](https://github.com/pardnchiu/QuickUI/blob/main/LICENSE). ## Author Just [open an issue](https://github.com/pardnchiu/QuickUI/issues/new) to share an idea. QuickUI contributors *** ©️ 2024 [邱敬幃 Pardn Chiu](https://www.linkedin.com/in/pardnchiu) --- Source: https://quickui.pardn.io/getting-started.md # Getting Started This page covers how to install QuickUI and bind your first data object to a page. ## Prerequisites | Item | Requirement | |---|---| | Browser | ES2022, `Proxy`, `IntersectionObserver` and `structuredClone` support | | HTTP server | Required when using external `i18n` files or ``; `fetch` fails under `file://` | | Node.js | Only needed when building from source | ## Installation ### CDN ```html ``` Loading the script registers three globals: `QUI`, `_` and `$`. See [Global Helpers](/global-helpers). ### npm ```bash npm i @pardnchiu/quickui ``` ### ESM ```javascript import { QUI } from "@pardnchiu/quickui"; ``` `package.json` points `module` at `dist/QuickUI.esm.js`, so bundlers resolve the bare package name to the ESM build. The ESM build is the same code as the CDN build with `window.QUI` exported at the end, so importing it still registers the globals. ### From Source ```bash git clone https://github.com/pardnchiu/QuickUI.git cd QuickUI npm install npm run build:once ``` `build:once` runs `tsc` to bundle `src/` into `src/QuickUI.debug.js`, then terser produces `dist/QuickUI.js` and `dist/QuickUI.esm.js`. ## Your First App ```demo

{{ title }}

{{ description }}

``` `id` points to an existing element whose content becomes the template. Changing `app.data.title` later re-renders the page about 300 ms after the change. ## Next Steps | Goal | Page | |---|---| | Learn the template syntax | [Text Binding](/text-binding), [Conditional Rendering](/conditional-rendering), [Loop Rendering](/loop-rendering) | | Handle user input | [Event Binding](/event-binding), [Two-way Binding](/two-way-binding) | | Look up every option | [Constructor Options](/constructor-options) | --- Source: https://quickui.pardn.io/architecture.md # Architecture This page shows how QuickUI's modules are layered and where each one lives in the source, in a single overview diagram. ## System Overview ```mermaid graph TB T["Template: element referenced by id / render()"] --> V["vDOM: :for → :if → :path → {{ }}"] D["data (Proxy)"] -->|"set triggers"| L["Lifecycle: 300 ms debounce"] L --> V I["i18n locale data"] --> V V --> P["diff → Patch[]"] P -->|"requestAnimationFrame"| R["Real DOM"] R --> O["Listeners: lazyload / temp-svg"] ``` ## Layers | Layer | Source | Responsibility | |---|---|---| | Entry | `src/model` (`QUI.ts`) | Creates the instance, loads i18n, wraps reactive data, applies patches, handles attribute and event binding | | Virtual DOM | `src/model` (`vDOM.ts`) | Turns elements into `tag` / `props` / `children` / `data`, expands `:for`, `:if`, `:path`, replaces `{{ }}`, produces patches | | Lifecycle | `src/model` (`Lifecycle.ts`) | Wraps the timing of the initial render and updates, runs `when` hooks, debounces updates by 300 ms | | Utilities | `src/function` | `createReactiveObject`, `htmlParser`, `createElement`, `getElement`, `calc`, `dateFormat` and more | | Listeners | `src/listener` | `IntersectionObserver`-driven image lazy loading and SVG inlining | | Shared constants | `src/*.ts` | Global aliases, regular expressions, directive names, the effect CSS injected into the page | `tsc` concatenates every file into one script, `src/QuickUI.debug.js`, through the `outFile` setting in `tsconfig.json`; there are no module imports. Public names are attached to the global object as `window.QUI`, `window._` and `window.$`. ## Cross-cutting Principles | Principle | Implementation | |---|---| | Zero dependencies | Only native browser APIs (`Proxy`, `fetch`, `IntersectionObserver`, `requestAnimationFrame`) | | The template is the DOM | Templates are plain HTML on the page with no compile step; every render rebuilds the vDOM from a copy of the original template | | Data snapshots | Each render takes a snapshot with `JSON.parse(JSON.stringify(data))`, so functions, `Date` objects and `undefined` never reach the template | | Batched writes | Patches are always applied inside `requestAnimationFrame` | ## Further Reading - Render flow in detail: [Rendering Pipeline](/rendering-pipeline) - Change detection: [Reactive Data](/reactive-data) - Per-module diagrams: [doc/architecture.md](https://github.com/pardnchiu/QuickUI/blob/main/doc/architecture.md) --- Source: https://quickui.pardn.io/rendering-pipeline.md # Rendering Pipeline This page walks through the steps a render takes from template to real DOM, and the order in which patches are applied. ## Initialization | Step | Behavior | |---|---| | 1 | Resolve the element by `id`; with no `id` but a `render` function, create a detached `
` | | 2 | Clone the element as the original template and build the first vDOM from the unprocessed template as the diff baseline | | 3 | When `render` is given, replace the template content with its output | | 4 | Load `i18n` (string values are fetched); only then are `app.data`, `app.event` and `app.body` assigned | | 5 | Enable the SVG and lazyload listeners according to `option`, create the `Lifecycle` and run the initial render | If the element is missing, or neither `id` nor `render` is given, an error is logged to the console and the instance renders nothing. ## Each Render | Stage | Behavior | |---|---| | Rebuild vDOM | Build a new vDOM from a copy of the original template, using a `JSON.parse(JSON.stringify(app.data))` snapshot as data | | Expand directives | Process `:for` → `:if` → ``, then run `:for` and `:if` again on content inserted by `:path` | | Replace text | Replace `{{ i18n.* }}` first, then every other `{{ }}` | | Diff | Compare the old and new vDOM child by child using indexes, producing patches | | Apply | Apply the patches inside `requestAnimationFrame`, then strip leftover `:if` attributes and handle [Render Effects](/effects) | ## Patch Types | Type | Trigger | Action | |---|---|---| | `REMOVE` | The new tree has no node at that position | Removed from the deepest, last position first so indexes do not shift | | `CREATE` | The old tree has no node | Create the element and append it to the parent | | `APPEND` | The number of children grows | Insert at the matching index | | `REPLACE` | The tag name differs at the same position | Replace with the new element | | `PROP` | Every attribute of every new node | Dispatched by prefix to [attribute](/attribute-binding), [event](/event-binding) or [form](/two-way-binding) handling | | `TEXT` | The text differs at the same position | Overwrite the text content | `REMOVE` patches always run before every other patch. ## Edge Cases | Situation | Result | |---|---| | Inserting at the front of a list | Diffing has no keys and aligns by index, so every item after the insertion point receives patches | | ` ``` All three writes trigger an update; writes within 300 ms of each other are merged into one render. ## What Triggers an Update | Write | Triggers | |---|---| | `app.data.key = value` (value changes) | Yes | | Nested properties, array indexes, `push` / `splice` | Yes | | Writing the same value | No | | `delete app.data.key` | No, the Proxy does not trap deletion | | Replacing `app.data = { ... }` | No, the replacement is not a Proxy | | `once: true` | No, data is not wrapped and renders once | ## When to Access It `app.data` is assigned only after i18n finishes loading; even without `i18n` it is deferred to the next microtask. Values written synchronously right after `new QUI()` land on a placeholder object and are lost. Read and write it inside the [`rendered` hook](/lifecycle) or an event handler. ## Template Lookup Rules | Rule | Description | |---|---| | Paths | Use `.` for nested values, for example `{{ user.name }}` | | Scope | The current `:for` item's data is searched first, then the root data | | Missing value | The `{{ }}` placeholder stays on the page as written | | Usable types | Strings, numbers, booleans, arrays, plain objects; functions and `Date` are lost in the snapshot | --- Source: https://quickui.pardn.io/text-binding.md # Text Binding This page covers writing data into text with `{{ }}` and inserting HTML with `:html`. ## `{{ value }}` ```demo

{{ title }}

{{ user.name }} · {{ LENGTH(items) }} items

``` | Rule | Description | |---|---| | Allowed characters | Letters, digits, underscores and `.`; may be wrapped in a [built-in function](/built-in-functions) | | Whitespace | Both `{{title}}` and `{{ title }}` work | | Missing value | `{{ title }}` stays on the page as written | | Localization | `{{ i18n.key }}` reads the current locale, see [i18n](/i18n) | | Position | Replaced only in text nodes; `{{ }}` inside plain attributes is left alone, use [Attribute Binding](/attribute-binding) for attributes | ## `:html` ```demo
``` `:html` writes the value to `innerHTML`; when no matching data exists, the attribute value itself is written. Content is not sanitized, so never pass unescaped user input, or the page is open to XSS. --- Source: https://quickui.pardn.io/built-in-functions.md # Built-in Functions This page lists the five functions available inside `{{ }}` and the limits of each. ## Function List | Function | Syntax | Result | |---|---|---| | `LENGTH()` | `{{ LENGTH(items) }}` | Length of an array or string; key count for objects | | `CALC()` | `{{ CALC(price * 1.05) }}` | Arithmetic between a data value and a number literal | | `UPPER()` | `{{ UPPER(name) }}` | Uppercase | | `LOWER()` | `{{ LOWER(name) }}` | Lowercase | | `DATE()` | `{{ DATE(timestamp, YYYY-MM-DD HH:mm) }}` | Formats a UNIX timestamp in seconds | Function names must be uppercase and only one level is allowed; functions cannot be nested. ## `CALC()` ```demo

{{ CALC(price * 1.05) }}

{{ CALC(count + 1) }}

``` Supported operators: `+`, `-`, `*`, `/`. ## `UPPER()` / `LOWER()` The value must be a string; a `null` or missing value throws a `TypeError` and aborts that render. ## `DATE()` ```demo

{{ DATE(now, YYYY-MM-DD HH:mm:ss) }}

``` | Token | Output | |---|---| | `YYYY` / `YY` | Four-digit / two-digit year | | `MM` / `M` | Zero-padded / unpadded month | | `DD` / `D` | Zero-padded / unpadded day | | `HH` / `H` | 24-hour clock | | `hh` / `h` | 12-hour clock | | `mm` / `m`, `ss` / `s` | Minutes, seconds | | `SSS` | Milliseconds | | `A` / `a` | `AM` / `PM`, `am` / `pm` | | `ddd` | Weekday; 星期一 when the browser language contains `zh`, otherwise the English abbreviation (Mon) | The format string only accepts letters, digits, spaces, `-`, `:` and `,`; with `/` or `.` it is not recognized as `DATE()` and `{{ }}` stays as written. --- Source: https://quickui.pardn.io/conditional-rendering.md # Conditional Rendering This page covers writing `:if`, `:else-if` and `:else`, the comparison operators available, and how conditions are evaluated. ## Basic Usage ```demo

Active

Pending

Disabled

``` | Directive | Description | |---|---| | `:if` | Starts a condition group | | `:else-if` / `:elif` | Must directly follow the previous condition element at the same level | | `:else` | Shown when no earlier condition holds | Only the first element whose condition holds is kept. When none holds and there is no `:else`, the whole group is removed. An `:else-if` or `:else` with no preceding `:if` is treated as a normal element and always shown. ## Comparison Operators | Operator | Comparison | Example | |---|---|---| | None | Truthiness of the value | `:if="isLogin"` | | `==` / `===` | Equal after converting to strings | `:if="status == active"` | | `!=` / `!==` | Not equal after converting to strings | `:if="status != disabled"` | | `>` / `<` | Numeric comparison | `:if="count > 0"` | | `>=` / `<=` | Numeric comparison | `:if="count <= 100"` | ## Special Comparison Values | Value | Holds with `==` when | Holds with `!=` when | |---|---|---| | `null` | The value is `null` or missing | The value exists | | `true` | The value is truthy | The value is falsy | | `false` | The value is falsy | The value is truthy | | `empty` | The value is an empty string or missing | The value is not empty | ## Evaluation Rules | Rule | Description | |---|---| | Right-hand side | Always a literal, never looked up in data; `count > max` compares against `"max"`, which becomes `0` | | Negation | `!flag` is not supported; write `flag == false` | | Compound conditions | `&&` and `||` are not supported; nest elements or precompute a boolean in data | | Combined with `:for` | `:for` expands first, then each item evaluates `:if` on its own and can use the item variable | ```demo
  • {{ user.name }}
``` --- Source: https://quickui.pardn.io/loop-rendering.md # Loop Rendering This page explains how `:for` expands arrays and objects, and which variables are reachable inside a loop. ## Arrays ```demo
  • {{ item }}
  • {{ index }}. {{ item }}
``` | Syntax | Variables | |---|---| | `item in list` | `item` is the element | | `(item, index) in list` | Adds a zero-based `index` | ## Objects ```demo
  • {{ key }}: {{ value }}
``` The first variable is the key and the second is the value; with a single variable you only get the key. ## Scope | Reachable | Description | |---|---| | Loop variables | `item`, `index` (or `key`, `value`) | | Source collection | Read from root data under the same name, for example `items` | | Root data | Looked up in `app.data` when the loop scope has no match | | Outer loop variables | Not reachable; the inner body of a nested loop only sees its own variables and root data | ```demo

{{ group.name }}

{{ member }}

``` The inner source path `group.members` may use the outer variable, but `{{ group.name }}` inside the inner element is not replaced. --- Source: https://quickui.pardn.io/attribute-binding.md # Attribute Binding This page covers binding data to HTML attributes with the `:` or `q-` prefix, and how `:hide` is evaluated. ## Syntax ```demo ``` | Prefix | Description | |---|---| | `:` | Standard form, for example `:href` | | `q-` | Equivalent form, for example `q-href`; useful for toolchains that reject `:` in attribute names | The attribute value is first looked up as a data path; when nothing matches, the value itself is used. When the value contains `{{ }}`, only the first `{{ }}` is replaced and the surrounding text is kept. The original `:` attribute is removed afterwards. ## Dispatch Rules | Attribute | Written as | |---|---| | `:id`, `:src`, `:alt`, `:href` | Element property | | `:html` | `innerHTML`, see [Text Binding](/text-binding) | | `:` prefix and the name matches a CSS property (such as `:width`, `:color`) | Written to `style`, see [Style Binding](/style-binding) | | `:hide` | Sets `display: none` when truthy | | `:lazyload` | See [Image Lazy Loading](/lazyload) | | Anything else (such as `:title`, `:data-id`, `:value`) | `setAttribute` with the prefix stripped | Names that match a CSS property always go to `style`: `` sets the CSS `width`, not the `width` attribute. ## `:hide` ```demo
Content
``` | Value | Result | |---|---| | `true`, non-zero numbers, non-empty strings | Hidden | | `false`, `0`, `"0"`, empty string | Shown; the value is written as a `hide` attribute (for example `hide="false"`) | | A key that does not exist in data | The attribute value itself (a non-empty string) is used, so the element is hidden | The string `"false"` is truthy and also hides the element; use booleans. Once hidden, setting the value back to a falsy one does not remove `display: none`, so the element stays hidden. Press Toggle twice in the preview above to reproduce it. ## Localized Attribute Values Any attribute whose value is `i18n.key` is replaced with the translation for the current locale, for example `placeholder="i18n.search"`. Attributes only support single-level keys, see [i18n](/i18n). --- Source: https://quickui.pardn.io/style-binding.md # Style Binding This page covers binding data straight to inline styles with `:[CSS property]`. ## Syntax ```demo
``` Result: ```html ``` | Rule | Description | |---|---| | Name | A kebab-case CSS property name whose camelCase form exists on `element.style` | | Value | The data value or the attribute value itself, including units, for example `"120px"` | | Prefix | Only `:`; `q-width` does not reach `style` and sets the `width` attribute instead | | `:animation` | Values `fade-in` and `expand` are not written to `style`; they are treated as [Render Effects](/effects) | ## Legacy Names The forms below still work but are planned for removal in 1.0.0; see [Deprecations](/deprecations). | Legacy | Use instead | |---|---| | `:bg-*` such as `:bg-color`, `:bg-image` | `:background-color`, `:background-image` | | camelCase such as `:borderRadius`, `:backgroundColor` | `:border-radius`, `:background-color` | --- Source: https://quickui.pardn.io/event-binding.md # Event Binding This page covers wiring DOM events to handlers in the `event` option with `@event`. ## Syntax ```demo
``` | Prefix | Example | Description | |---|---|---| | `@` | `@click` | Standard form | | `qe-` | `qe-click` | Equivalent form | `@click="save"` becomes `element.onclick = app.event.save`, and the `@click` attribute is removed afterwards. ## Rules | Rule | Description | |---|---| | Event name | Must be a native event with a matching `on*` property on the element (`click`, `input`, `change`, `submit`, `keyup` and so on); custom events never fire | | Handler arguments | Only the `Event`; with a `function` declaration, `this` is the element | | One handler per event | Assigned as a property, so an element keeps only one handler per event | | Name lookup | The value is first looked up as a data path and the result is used as the handler name; otherwise the value itself is the name | | Unknown name | `undefined` is assigned; clicking does nothing and no error is raised | | `:@click` | Not supported; the attribute is removed and nothing is bound | ## Getting the Item Inside a Loop Handlers do not receive the `:for` item, so put the identifier in a `data-*` attribute: ```demo
``` ## Using Handlers Outside QuickUI `app.event` keeps the original function map, so handlers can be assigned to elements you create yourself: ```javascript const button = document.createElement("button"); button.onclick = app.event.save; app.body.append(button); ``` --- Source: https://quickui.pardn.io/two-way-binding.md # Two-way Binding This page explains how `:model` writes form input back into data. ## Syntax ```demo

Hello, {{ username }}

``` ## Behavior per Element | Element | Events | Value written to `app.data[key]` | |---|---|---| | Text ``, `