# 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
```
`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 ``, `