odac 1.4.4 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/rules/memory.md +10 -1
- package/.releaserc.js +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +2 -1
- package/client/odac.js +228 -62
- package/docs/ai/skills/backend/views.md +102 -30
- package/docs/ai/skills/frontend/core.md +17 -3
- package/docs/ai/skills/frontend/navigation.md +105 -8
- package/docs/backend/07-views/01-the-view-directory.md +28 -6
- package/docs/backend/07-views/02-rendering-a-view.md +16 -23
- package/docs/backend/07-views/03-template-syntax.md +48 -14
- package/docs/backend/07-views/03-variables.md +22 -7
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +22 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +51 -0
- package/package.json +30 -6
- package/src/Request.js +5 -4
- package/src/Route.js +11 -0
- package/src/View.js +54 -9
- package/template/controller/page/about.js +3 -3
- package/template/controller/page/index.js +2 -2
- package/template/public/assets/js/app.js +38 -54
- package/template/skeleton/main.html +4 -4
- package/template/view/content/about.html +64 -60
- package/template/view/content/home.html +148 -175
- package/template/view/css/app.css +46 -0
- package/template/view/footer/main.html +10 -9
- package/template/view/header/main.html +34 -11
- package/test/Client/load.test.js +306 -0
- package/test/Database/ConnectionFactory/buildConnections.test.js +4 -0
- package/test/View/parseOdacTag.test.js +180 -0
- package/template/public/assets/css/style.css +0 -1835
|
@@ -60,11 +60,25 @@ Odac.action({
|
|
|
60
60
|
### 3. Data Utilities
|
|
61
61
|
```javascript
|
|
62
62
|
// Accessing data shared from backend (Odac.share)
|
|
63
|
-
const user = odac.data('user')
|
|
63
|
+
const user = odac.data('user')
|
|
64
64
|
|
|
65
65
|
// Using Storage wrapper
|
|
66
|
-
odac.storage('theme', 'dark')
|
|
67
|
-
const theme = odac.storage('theme')
|
|
66
|
+
odac.storage('theme', 'dark')
|
|
67
|
+
const theme = odac.storage('theme')
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Programmatic Navigation
|
|
71
|
+
```javascript
|
|
72
|
+
// Navigate to a URL (AJAX, pushes to history)
|
|
73
|
+
Odac.load('/dashboard')
|
|
74
|
+
|
|
75
|
+
// Navigate without history entry
|
|
76
|
+
Odac.load('/dashboard', null, false)
|
|
77
|
+
|
|
78
|
+
// Navigate with a post-load callback
|
|
79
|
+
Odac.load('/dashboard', function(page, vars) {
|
|
80
|
+
console.log('Loaded page:', page)
|
|
81
|
+
})
|
|
68
82
|
```
|
|
69
83
|
|
|
70
84
|
## Best Practices
|
|
@@ -1,27 +1,124 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontend-navigation-spa-skill
|
|
3
|
-
description: Single-page navigation patterns in odac.js
|
|
3
|
+
description: Single-page navigation patterns in odac.js including smart part diffing, smooth transitions, route control, and lifecycle-safe execution.
|
|
4
4
|
metadata:
|
|
5
|
-
tags: frontend, navigation, spa, ajax-navigation, page-lifecycle, transitions
|
|
5
|
+
tags: frontend, navigation, spa, ajax-navigation, page-lifecycle, transitions, view-transitions, part-diffing
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Frontend Navigation & SPA Skill
|
|
9
9
|
|
|
10
10
|
Smooth transitions and single-page application behavior using `odac.js`.
|
|
11
11
|
|
|
12
|
+
## How AJAX Navigation Works
|
|
13
|
+
|
|
14
|
+
ODAC's navigation system is **zero-config** and **server-driven**. On first page load, the framework automatically:
|
|
15
|
+
1. Detects all skeleton placeholder wrapper elements (those with `data-odac-navigate` attributes injected by the server).
|
|
16
|
+
2. Registers click handlers on all internal links.
|
|
17
|
+
3. Reads the initial parts state from `data-odac-parts` on the `<html>` element.
|
|
18
|
+
|
|
19
|
+
On every subsequent navigation:
|
|
20
|
+
1. The client sends the current parts state to the server via `X-Odac-Parts`.
|
|
21
|
+
2. The server returns only the parts that changed (`output`) plus the new parts manifest (`parts`).
|
|
22
|
+
3. The client fades out and updates only the changed elements. Unchanged parts (e.g. a shared sidebar) are never touched.
|
|
23
|
+
|
|
12
24
|
## Rules
|
|
13
|
-
1. **
|
|
14
|
-
2. **
|
|
15
|
-
3. **
|
|
25
|
+
1. **Zero-config auto-navigation**: Works automatically when the skeleton has `data-odac-navigate` elements. No `Odac.action()` call needed for basic navigation.
|
|
26
|
+
2. **Manual setup**: Use `Odac.action({ navigate: ... })` to customize selectors, update targets, or add callbacks.
|
|
27
|
+
3. **Exclusion**: Use `data-navigate="false"` attribute or `.no-navigate` class on links to force full page reloads.
|
|
28
|
+
4. **Lifecycle**: Use `load` and `page` events to run code after navigation.
|
|
29
|
+
5. **View Transitions**: Add `odac-transition` attribute to elements for native browser View Transition API animations.
|
|
16
30
|
|
|
17
31
|
## Patterns
|
|
32
|
+
|
|
33
|
+
### Auto-navigation (zero-config)
|
|
34
|
+
No setup required. As long as the skeleton has properly wrapped placeholders, navigation is automatic.
|
|
35
|
+
|
|
36
|
+
### Manual navigation setup
|
|
18
37
|
```javascript
|
|
19
38
|
Odac.action({
|
|
20
39
|
navigate: {
|
|
21
|
-
update: 'main',
|
|
40
|
+
update: 'main', // CSS selector of the element to update
|
|
22
41
|
on: function(page, vars) {
|
|
23
|
-
console.log('Navigated to:', page)
|
|
42
|
+
console.log('Navigated to:', page)
|
|
24
43
|
}
|
|
25
44
|
}
|
|
26
|
-
})
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Programmatic navigation
|
|
49
|
+
```javascript
|
|
50
|
+
// Navigate to a URL programmatically
|
|
51
|
+
Odac.load('/docs/getting-started')
|
|
52
|
+
|
|
53
|
+
// Navigate without pushing to history
|
|
54
|
+
Odac.load('/docs/getting-started', null, false)
|
|
55
|
+
|
|
56
|
+
// Navigate with a callback
|
|
57
|
+
Odac.load('/docs/getting-started', function(page, vars) {
|
|
58
|
+
console.log('Loaded:', page)
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Excluding links from AJAX navigation
|
|
63
|
+
```html
|
|
64
|
+
<!-- Full reload for this link -->
|
|
65
|
+
<a href="/logout" data-navigate="false">Logout</a>
|
|
66
|
+
|
|
67
|
+
<!-- Full reload via class -->
|
|
68
|
+
<a href="/external-page" class="no-navigate">External</a>
|
|
27
69
|
```
|
|
70
|
+
|
|
71
|
+
## Smart Part Diffing Behavior
|
|
72
|
+
|
|
73
|
+
The client automatically handles these scenarios without any configuration:
|
|
74
|
+
|
|
75
|
+
| Scenario | Client behavior |
|
|
76
|
+
|----------|----------------|
|
|
77
|
+
| Sidebar unchanged between pages | Sidebar DOM untouched, no flicker |
|
|
78
|
+
| Sidebar changes between pages | Sidebar fades out → new content → fades in |
|
|
79
|
+
| Sidebar removed on new page | Sidebar element content cleared |
|
|
80
|
+
| Skeleton changes | Full page reload |
|
|
81
|
+
| `content` part | Always updated |
|
|
82
|
+
|
|
83
|
+
The fade animation only runs on elements that actually receive new content. Unchanged parts stay fully visible throughout the navigation.
|
|
84
|
+
|
|
85
|
+
## View Transitions (Native Browser API)
|
|
86
|
+
|
|
87
|
+
Elements with the `odac-transition` attribute automatically use the browser's View Transition API instead of the legacy fade animation. The attribute value becomes the `view-transition-name`, enabling per-element morphing between pages.
|
|
88
|
+
|
|
89
|
+
### HTML Usage
|
|
90
|
+
```html
|
|
91
|
+
<header odac-transition="header">Site Header</header>
|
|
92
|
+
<nav odac-transition="sidebar">Navigation</nav>
|
|
93
|
+
<main>Content updated by AJAX loader</main>
|
|
94
|
+
<img odac-transition="hero" src="/hero.jpg" />
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Behavior
|
|
98
|
+
- When `odac-transition` elements exist and the browser supports View Transition API → native transition is used.
|
|
99
|
+
- When no `odac-transition` elements exist or the API is unsupported → legacy fade fallback runs automatically.
|
|
100
|
+
- Transition names are applied before the snapshot and cleaned up after the transition completes.
|
|
101
|
+
- The attribute value must be unique per page (browser requirement for `view-transition-name`).
|
|
102
|
+
|
|
103
|
+
### CSS Customization
|
|
104
|
+
```css
|
|
105
|
+
::view-transition-old(hero) {
|
|
106
|
+
animation: fade-out 0.3s ease;
|
|
107
|
+
}
|
|
108
|
+
::view-transition-new(hero) {
|
|
109
|
+
animation: fade-in 0.3s ease;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
::view-transition-old(sidebar) {
|
|
113
|
+
animation: slide-out-left 0.25s ease;
|
|
114
|
+
}
|
|
115
|
+
::view-transition-new(sidebar) {
|
|
116
|
+
animation: slide-in-left 0.25s ease;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Rules
|
|
121
|
+
1. Each `odac-transition` value must be unique within the page.
|
|
122
|
+
2. Elements that persist across navigations (e.g. shared header) produce smooth morphing animations.
|
|
123
|
+
3. No JavaScript configuration needed — attribute-only setup.
|
|
124
|
+
4. Falls back to fade animation gracefully on unsupported browsers.
|
|
@@ -42,15 +42,16 @@ Example: `skeleton/main.html`
|
|
|
42
42
|
|
|
43
43
|
**Important Rules for Placeholders:**
|
|
44
44
|
|
|
45
|
-
1. **Each placeholder must be wrapped in HTML
|
|
46
|
-
2. **Never place placeholders directly next to each other**
|
|
47
|
-
3. **Placeholders are uppercase**
|
|
48
|
-
4. **Use semantic HTML tags**
|
|
45
|
+
1. **Each placeholder must be wrapped in its own HTML tag** — This allows the AJAX navigation system to identify and independently update each section.
|
|
46
|
+
2. **Never place placeholders directly next to each other** — Bad: `{{ HEADER }}{{ CONTENT }}`, Good: `<header>{{ HEADER }}</header><main>{{ CONTENT }}</main>`
|
|
47
|
+
3. **Placeholders are uppercase** — `{{ HEADER }}`, `{{ CONTENT }}`, `{{ FOOTER }}`
|
|
48
|
+
4. **Use semantic HTML tags** — `<header>`, `<main>`, `<footer>`, `<aside>`, `<nav>`, etc.
|
|
49
|
+
5. **Unset placeholders are automatically removed** — If a controller does not call `set('sidebar', ...)`, the `{{ SIDEBAR }}` placeholder is silently removed from the output. No stale text leaks into the HTML.
|
|
49
50
|
|
|
50
51
|
**Why wrap in tags?**
|
|
51
|
-
When using AJAX navigation, the system
|
|
52
|
+
When using AJAX navigation, the system automatically injects `data-odac-navigate` attributes onto the wrapper elements of each placeholder. This enables the smart part-diffing engine to update only the sections that actually changed between navigations.
|
|
52
53
|
|
|
53
|
-
**Note:** Skeleton files
|
|
54
|
+
**Note:** Skeleton files support only view part placeholders (uppercase). For dynamic content like page titles, use a view part for the `<head>` section or place a `<title>` tag inside the content view.
|
|
54
55
|
|
|
55
56
|
### View Files
|
|
56
57
|
|
|
@@ -66,8 +67,29 @@ view/
|
|
|
66
67
|
├── content/
|
|
67
68
|
│ ├── home.html
|
|
68
69
|
│ └── about.html
|
|
70
|
+
├── sidebar/
|
|
71
|
+
│ └── docs.html
|
|
69
72
|
└── footer/
|
|
70
73
|
└── main.html
|
|
71
74
|
```
|
|
72
75
|
|
|
76
|
+
### Smart AJAX Navigation & Part Diffing
|
|
73
77
|
|
|
78
|
+
When navigating between pages via AJAX, ODAC uses a **server-driven part diffing** system to minimize unnecessary work:
|
|
79
|
+
|
|
80
|
+
- **Unchanged parts are skipped** — If `sidebar` points to the same view file on both the current and next page, the server does not re-render it and the client does not update its DOM. The sidebar stays visible and untouched.
|
|
81
|
+
- **Changed parts are updated** — Only parts whose view path changed are rendered and sent to the client.
|
|
82
|
+
- **Removed parts are cleared** — If the next page does not set a part that the current page had (e.g. navigating away from a page with a sidebar), that element's content is emptied.
|
|
83
|
+
- **`content` is always refreshed** — Because content views are typically URL-dependent (e.g. `/{id}`), the `content` part is always re-rendered regardless of view path.
|
|
84
|
+
- **Skeleton change triggers full reload** — If the next page uses a different skeleton, a full page navigation is performed automatically.
|
|
85
|
+
|
|
86
|
+
This means a shared sidebar, header, or footer that does not change between pages will never flicker or reload during AJAX navigation.
|
|
87
|
+
|
|
88
|
+
#### Force-refresh a part
|
|
89
|
+
|
|
90
|
+
If a part's view path stays the same but its rendered output changes per request (e.g. a sidebar with an active menu state), mark it with `{ refresh: true }`:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// This sidebar will re-render on every navigation even if the view path is unchanged
|
|
94
|
+
Odac.View.set('sidebar', 'docs.nav', { refresh: true })
|
|
95
|
+
```
|
|
@@ -70,6 +70,17 @@ Odac.View
|
|
|
70
70
|
|
|
71
71
|
In this case, placeholders like `{{ HEADER }}`, `{{ CONTENT }}`, `{{ FOOTER }}` in the skeleton are automatically matched with `view/home/header.html`, `view/home/content.html`, `view/home/footer.html` files.
|
|
72
72
|
|
|
73
|
+
### 6. Force-Refreshing a Part
|
|
74
|
+
|
|
75
|
+
By default, ODAC's smart diffing skips re-rendering a part if its view path hasn't changed between navigations. If a part's output is request-dependent (e.g. a sidebar that highlights the active menu item), use `{ refresh: true }` to always re-render it:
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// Sidebar re-renders on every AJAX navigation regardless of view path
|
|
79
|
+
Odac.View.set('sidebar', 'docs.nav', { refresh: true })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This option is only relevant for AJAX navigations. Full page loads always render all parts.
|
|
83
|
+
|
|
73
84
|
### Setting Dynamic Page Titles and Meta Tags
|
|
74
85
|
|
|
75
86
|
Since skeleton files only support view part placeholders, you have two approaches for dynamic titles:
|
|
@@ -101,8 +112,6 @@ Create a separate view part for the `<head>` section:
|
|
|
101
112
|
</html>
|
|
102
113
|
```
|
|
103
114
|
|
|
104
|
-
**Note:** Each placeholder is wrapped in an HTML tag so AJAX can identify and update specific sections.
|
|
105
|
-
|
|
106
115
|
**Head View (view/head/main.html):**
|
|
107
116
|
```html
|
|
108
117
|
<head>
|
|
@@ -122,15 +131,13 @@ module.exports = async function (Odac) {
|
|
|
122
131
|
.where('id', productId)
|
|
123
132
|
.first()
|
|
124
133
|
|
|
125
|
-
// Set dynamic title and description
|
|
126
134
|
Odac.pageTitle = product ? `${product.name} - My Store` : 'Product Not Found'
|
|
127
135
|
Odac.pageDescription = product ? product.short_description : ''
|
|
128
|
-
|
|
129
136
|
Odac.product = product
|
|
130
137
|
|
|
131
138
|
Odac.View.set({
|
|
132
139
|
skeleton: 'main',
|
|
133
|
-
head: 'main',
|
|
140
|
+
head: 'main',
|
|
134
141
|
header: 'main',
|
|
135
142
|
content: 'product.detail',
|
|
136
143
|
footer: 'main'
|
|
@@ -142,21 +149,6 @@ module.exports = async function (Odac) {
|
|
|
142
149
|
|
|
143
150
|
Include the title tag in your content view:
|
|
144
151
|
|
|
145
|
-
**Skeleton (skeleton/simple.html):**
|
|
146
|
-
```html
|
|
147
|
-
<!DOCTYPE html>
|
|
148
|
-
<html lang="en">
|
|
149
|
-
<head>
|
|
150
|
-
<meta charset="UTF-8">
|
|
151
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
152
|
-
<link rel="stylesheet" href="/assets/css/style.css">
|
|
153
|
-
</head>
|
|
154
|
-
<body>
|
|
155
|
-
{{ CONTENT }}
|
|
156
|
-
</body>
|
|
157
|
-
</html>
|
|
158
|
-
```
|
|
159
|
-
|
|
160
152
|
**Content View (view/content/product.html):**
|
|
161
153
|
```html
|
|
162
154
|
<title>{{ Odac.product.name }} - My Store</title>
|
|
@@ -167,7 +159,7 @@ Include the title tag in your content view:
|
|
|
167
159
|
</div>
|
|
168
160
|
```
|
|
169
161
|
|
|
170
|
-
|
|
162
|
+
The AJAX navigation system automatically extracts the `<title>` tag from the rendered content and updates `document.title`.
|
|
171
163
|
|
|
172
164
|
### Important Notes
|
|
173
165
|
|
|
@@ -175,5 +167,6 @@ Include the title tag in your content view:
|
|
|
175
167
|
- Skeleton files should be in the `skeleton/` directory, view files in the `view/` directory
|
|
176
168
|
- Placeholders for view parts are written in uppercase: `{{ HEADER }}`, `{{ CONTENT }}`, etc.
|
|
177
169
|
- View part names are specified in lowercase: `header`, `content`, etc.
|
|
178
|
-
- Variables in
|
|
179
|
-
-
|
|
170
|
+
- Variables in views are accessed via the `Odac` object: `{{ Odac.variableName }}`
|
|
171
|
+
- Unset placeholders are silently removed from the final HTML output
|
|
172
|
+
- You don't need to call `return` from the controller — `Odac.View.set()` automatically initiates rendering
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
## 🔧 Template Syntax Overview
|
|
2
2
|
|
|
3
|
-
Odac uses a powerful template engine to create dynamic content in view files. The engine provides
|
|
3
|
+
Odac uses a powerful template engine to create dynamic content in view files. The engine provides two equivalent syntaxes for displaying variables, plus dedicated tags for conditionals, loops, translations, and more.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Two Syntaxes, One Engine
|
|
6
|
+
|
|
7
|
+
ODAC offers two ways to output variables. Both are HTML-escaped (XSS-safe) and compile to the same engine code. The choice is about readability, not functionality.
|
|
8
|
+
|
|
9
|
+
| Syntax | Best For | Example |
|
|
10
|
+
|--------|----------|---------|
|
|
11
|
+
| `<odac var="x" />` | Standalone block output where the variable is the main content | `<h1><odac var="title" /></h1>` |
|
|
12
|
+
| `{{ x }}` | Attributes, inline text, and mixed HTML where tag syntax would be verbose | `<img src="{{ photo.url }}" alt="{{ photo.caption }}">` |
|
|
13
|
+
|
|
14
|
+
> **Guideline:** Use `{{ }}` inside HTML attributes (`src`, `href`, `alt`, `class`, `value`, etc.) and for inline text interpolation. Use `<odac var>` for standalone element content. Both are equally supported and recommended.
|
|
6
15
|
|
|
7
16
|
### Quick Reference
|
|
8
17
|
|
|
@@ -13,10 +22,17 @@ This page provides a quick overview of all available template features. For deta
|
|
|
13
22
|
Display data passed from controllers using `Odac.set()`:
|
|
14
23
|
|
|
15
24
|
```html
|
|
16
|
-
<!--
|
|
17
|
-
<odac var="username"
|
|
25
|
+
<!-- Standalone block output — prefer <odac var> -->
|
|
26
|
+
<h1><odac var="username" /></h1>
|
|
27
|
+
|
|
28
|
+
<!-- Inside attributes — prefer {{ }} -->
|
|
29
|
+
<img src="{{ product.image }}" alt="{{ product.name }}">
|
|
30
|
+
<a href="/user/{{ user.id }}">Profile</a>
|
|
18
31
|
|
|
19
|
-
<!--
|
|
32
|
+
<!-- Inline text — prefer {{ }} -->
|
|
33
|
+
<p>Welcome, {{ user.name }}. You have {{ count }} items.</p>
|
|
34
|
+
|
|
35
|
+
<!-- Raw HTML output (trusted content only) -->
|
|
20
36
|
<odac var="htmlContent" raw />
|
|
21
37
|
|
|
22
38
|
<!-- String literals -->
|
|
@@ -163,8 +179,10 @@ Full access to the Odac object in templates:
|
|
|
163
179
|
|
|
164
180
|
| Feature | Syntax | Documentation |
|
|
165
181
|
|---------|--------|---------------|
|
|
166
|
-
| Variable (
|
|
167
|
-
|
|
|
182
|
+
| Variable (standalone) | `<odac var="x" />` | [Variables](./03-variables.md) |
|
|
183
|
+
| Variable (inline/attribute) | `{{ x }}` | [Variables](./03-variables.md) |
|
|
184
|
+
| Raw HTML (tag) | `<odac var="x" raw />` | [Variables](./03-variables.md) |
|
|
185
|
+
| Raw HTML (inline) | `{!! x !!}` | [Variables](./03-variables.md) |
|
|
168
186
|
| String | `<odac>text</odac>` | [Variables](./03-variables.md) |
|
|
169
187
|
| Query Parameter | `<odac get="key" />` | [Request Data](./04-request-data.md) |
|
|
170
188
|
| Translation | `<odac translate>key</odac>` | [Translations](./07-translations.md) |
|
|
@@ -180,18 +198,34 @@ Full access to the Odac object in templates:
|
|
|
180
198
|
| Comment | `<!--odac ... odac-->` | [Comments](./09-comments.md) |
|
|
181
199
|
| Image | `<odac:img src="..." />` | [Image Optimization](./11-image-optimization.md) |
|
|
182
200
|
|
|
183
|
-
###
|
|
201
|
+
### Syntax Selection Guide
|
|
202
|
+
|
|
203
|
+
Choose the right syntax based on context for clean, readable templates:
|
|
184
204
|
|
|
185
205
|
```html
|
|
186
|
-
<!--
|
|
187
|
-
{{
|
|
206
|
+
<!-- ✅ Attributes — use {{ }} -->
|
|
207
|
+
<img src="{{ product.image }}" alt="{{ product.name }}">
|
|
208
|
+
<a href="/products/{{ product.id }}" class="card {{ isActive ? 'active' : '' }}">
|
|
209
|
+
<input type="text" name="search" value="{{ query }}">
|
|
210
|
+
|
|
211
|
+
<!-- ✅ Inline text — use {{ }} -->
|
|
212
|
+
<p>Hello, {{ user.name }}. You have {{ count }} notifications.</p>
|
|
213
|
+
<span>${{ product.price }}</span>
|
|
214
|
+
|
|
215
|
+
<!-- ✅ Standalone block content — use <odac var> -->
|
|
216
|
+
<h1><odac var="pageTitle" /></h1>
|
|
217
|
+
<td><odac var="user.email" /></td>
|
|
218
|
+
|
|
219
|
+
<!-- ✅ Raw output — either form works -->
|
|
220
|
+
<div><odac var="richContent" raw /></div>
|
|
221
|
+
<div>{!! richContent !!}</div>
|
|
222
|
+
```
|
|
188
223
|
|
|
189
|
-
|
|
190
|
-
{!! htmlContent !!}
|
|
224
|
+
### Legacy Comment Syntax
|
|
191
225
|
|
|
192
|
-
|
|
226
|
+
```html
|
|
193
227
|
{{-- This is a comment --}}
|
|
194
228
|
```
|
|
195
229
|
|
|
196
|
-
**Note:**
|
|
230
|
+
**Note:** For new projects, prefer the `<!--odac ... odac-->` comment syntax for consistency.
|
|
197
231
|
|
|
@@ -311,18 +311,33 @@ module.exports = async function(Odac) {
|
|
|
311
311
|
</odac:if>
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
-
###
|
|
314
|
+
### Inline Syntax (`{{ }}` and `{!! !!}`)
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
ODAC provides an inline interpolation syntax that is equivalent to the `<odac var>` tag. Both compile to the same engine output and are equally supported.
|
|
317
|
+
|
|
318
|
+
**Use `{{ }}` when the variable appears inside HTML attributes or inline within text:**
|
|
317
319
|
|
|
318
320
|
```html
|
|
319
|
-
<!--
|
|
320
|
-
{{
|
|
321
|
-
{{ user.
|
|
321
|
+
<!-- Inside attributes — {{ }} keeps markup clean -->
|
|
322
|
+
<img src="{{ product.image }}" alt="{{ product.name }}">
|
|
323
|
+
<a href="/user/{{ user.id }}" class="btn {{ isActive ? 'active' : '' }}">Profile</a>
|
|
324
|
+
<input type="text" name="q" value="{{ searchQuery }}">
|
|
325
|
+
|
|
326
|
+
<!-- Inline text interpolation -->
|
|
327
|
+
<p>Welcome, {{ user.name }}. You have {{ notifications }} new messages.</p>
|
|
328
|
+
<span>${{ product.price }}</span>
|
|
322
329
|
|
|
323
|
-
<!-- Raw
|
|
330
|
+
<!-- Raw inline output (trusted content only) -->
|
|
324
331
|
{!! htmlContent !!}
|
|
325
332
|
{!! user.bio !!}
|
|
326
333
|
```
|
|
327
334
|
|
|
328
|
-
**
|
|
335
|
+
**Use `<odac var>` when the variable is the standalone content of an element:**
|
|
336
|
+
|
|
337
|
+
```html
|
|
338
|
+
<h1><odac var="pageTitle" /></h1>
|
|
339
|
+
<td><odac var="user.email" /></td>
|
|
340
|
+
<p><odac var="product.description" /></p>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Both syntaxes are XSS-safe by default (HTML-escaped). The choice is purely about readability. See [Template Syntax Overview](./03-template-syntax.md) for the full selection guide.
|
|
@@ -6,6 +6,7 @@ Odac framework includes a built-in AJAX navigation system that enables smooth, s
|
|
|
6
6
|
|
|
7
7
|
- **Zero Configuration**: Works automatically with all internal links (`/`)
|
|
8
8
|
- **Smooth Transitions**: Load only specific page sections without full page reload
|
|
9
|
+
- **Native View Transitions**: Automatic browser View Transition API support via `odac-transition` attribute
|
|
9
10
|
- **History API Integration**: Browser back/forward buttons work seamlessly
|
|
10
11
|
- **Automatic Token Management**: CSRF tokens are handled automatically
|
|
11
12
|
- **Progressive Enhancement**: Falls back to normal navigation if JavaScript fails
|
|
@@ -178,6 +179,27 @@ module.exports = function (Odac) {
|
|
|
178
179
|
5. Browser URL updates via History API
|
|
179
180
|
6. Page-specific callbacks execute
|
|
180
181
|
|
|
182
|
+
### View Transition Load (with `odac-transition` elements)
|
|
183
|
+
|
|
184
|
+
When elements with `odac-transition` attribute exist on the page and the browser supports the View Transition API, ODAC uses native transitions instead of fade:
|
|
185
|
+
|
|
186
|
+
1. User clicks `<a href="/about">`
|
|
187
|
+
2. ODAC assigns `view-transition-name` to all `odac-transition` elements (old state snapshot)
|
|
188
|
+
3. AJAX request is sent (same as above)
|
|
189
|
+
4. `document.startViewTransition()` is called — browser captures old state
|
|
190
|
+
5. DOM is updated with new content inside the transition callback
|
|
191
|
+
6. New `odac-transition` elements receive their names
|
|
192
|
+
7. Browser animates between old and new snapshots
|
|
193
|
+
8. Transition names are cleaned up after completion
|
|
194
|
+
|
|
195
|
+
No configuration needed — just add the attribute to your HTML:
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<header odac-transition="header">{{ HEADER }}</header>
|
|
199
|
+
<main>{{ CONTENT }}</main>
|
|
200
|
+
<img odac-transition="hero" src="/hero.jpg" alt="Hero" />
|
|
201
|
+
```
|
|
202
|
+
|
|
181
203
|
**Key Points:**
|
|
182
204
|
- The `output` keys in the JSON response match the lowercase keys from `Odac.View.set()` in your controller
|
|
183
205
|
- These keys correspond to UPPERCASE placeholders in your skeleton (e.g., `content` → `{{ CONTENT }}`)
|
|
@@ -143,6 +143,57 @@ Odac.action({
|
|
|
143
143
|
|
|
144
144
|
## Animation & Transitions
|
|
145
145
|
|
|
146
|
+
### View Transitions (Recommended)
|
|
147
|
+
|
|
148
|
+
ODAC natively supports the browser's [View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API). Add the `odac-transition` attribute to any element that should animate between page navigations. No JavaScript configuration is needed.
|
|
149
|
+
|
|
150
|
+
```html
|
|
151
|
+
<header odac-transition="header">Site Header</header>
|
|
152
|
+
<nav odac-transition="sidebar">Navigation</nav>
|
|
153
|
+
<main>Regular content (updated by AJAX loader)</main>
|
|
154
|
+
<img odac-transition="hero" src="/hero.jpg" alt="Hero" />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**How it works:**
|
|
158
|
+
- Before navigation, ODAC assigns `view-transition-name` to each `odac-transition` element
|
|
159
|
+
- The browser captures a snapshot of the old state
|
|
160
|
+
- DOM is updated with new content
|
|
161
|
+
- New `odac-transition` elements receive their transition names
|
|
162
|
+
- The browser animates between old and new snapshots
|
|
163
|
+
|
|
164
|
+
**Rules:**
|
|
165
|
+
1. Each `odac-transition` value must be unique within the page (browser requirement)
|
|
166
|
+
2. Elements that persist across pages (e.g., shared header) will morph smoothly
|
|
167
|
+
3. If the browser doesn't support View Transition API, the legacy fade animation runs automatically
|
|
168
|
+
|
|
169
|
+
**CSS Customization:**
|
|
170
|
+
|
|
171
|
+
```css
|
|
172
|
+
/* Crossfade the hero image */
|
|
173
|
+
::view-transition-old(hero) {
|
|
174
|
+
animation: fade-out 0.3s ease;
|
|
175
|
+
}
|
|
176
|
+
::view-transition-new(hero) {
|
|
177
|
+
animation: fade-in 0.3s ease;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Slide the sidebar */
|
|
181
|
+
::view-transition-old(sidebar) {
|
|
182
|
+
animation: slide-out-left 0.25s ease;
|
|
183
|
+
}
|
|
184
|
+
::view-transition-new(sidebar) {
|
|
185
|
+
animation: slide-in-left 0.25s ease;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Default transition for all elements */
|
|
189
|
+
::view-transition-old(*) {
|
|
190
|
+
animation-duration: 0.2s;
|
|
191
|
+
}
|
|
192
|
+
::view-transition-new(*) {
|
|
193
|
+
animation-duration: 0.2s;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
146
197
|
### Custom Transitions
|
|
147
198
|
|
|
148
199
|
Add custom animations:
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "odac",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Lightweight, high-performance Node.js framework for building modern web applications with built-in routing, auth, database, templating, WebSocket, i18n and zero-config Tailwind CSS.",
|
|
4
4
|
"homepage": "https://odac.run",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "emre.red",
|
|
7
7
|
"email": "mail@emre.red",
|
|
8
8
|
"url": "https://emre.red"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.4.
|
|
10
|
+
"version": "1.4.6",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=18.0.0"
|
|
@@ -27,14 +27,38 @@
|
|
|
27
27
|
"tailwindcss": "^4.1.18"
|
|
28
28
|
},
|
|
29
29
|
"optionalDependencies": {
|
|
30
|
+
"sharp": "^0.33.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
30
33
|
"mysql2": "^3.16.0",
|
|
31
34
|
"pg": "^8.16.3",
|
|
32
35
|
"redis": "^5.10.0",
|
|
33
|
-
"
|
|
36
|
+
"sqlite3": "^6.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"mysql2": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"pg": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"redis": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"sqlite3": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
34
51
|
},
|
|
35
52
|
"overrides": {
|
|
36
|
-
"
|
|
37
|
-
"cross-spawn": "7.0.6"
|
|
53
|
+
"brace-expansion": "5.0.5",
|
|
54
|
+
"cross-spawn": "7.0.6",
|
|
55
|
+
"handlebars": "4.7.9",
|
|
56
|
+
"minimatch": "10.2.4",
|
|
57
|
+
"test-exclude": {
|
|
58
|
+
"minimatch": "3.1.5"
|
|
59
|
+
},
|
|
60
|
+
"picomatch": "4.0.4",
|
|
61
|
+
"tar": "7.5.13"
|
|
38
62
|
},
|
|
39
63
|
"devDependencies": {
|
|
40
64
|
"@eslint/js": "^9.39.2",
|
|
@@ -54,7 +78,7 @@
|
|
|
54
78
|
"lint-staged": "^16.2.7",
|
|
55
79
|
"prettier": "^3.8.1",
|
|
56
80
|
"semantic-release": "^25.0.3",
|
|
57
|
-
"sqlite3": "^
|
|
81
|
+
"sqlite3": "^6.0.1"
|
|
58
82
|
},
|
|
59
83
|
"scripts": {
|
|
60
84
|
"lint": "eslint .",
|
package/src/Request.js
CHANGED
|
@@ -16,6 +16,7 @@ class OdacRequest {
|
|
|
16
16
|
isAjaxLoad = false
|
|
17
17
|
ajaxLoad = null
|
|
18
18
|
clientSkeleton = null
|
|
19
|
+
clientParts = null
|
|
19
20
|
page = null
|
|
20
21
|
|
|
21
22
|
constructor(id, req, res, odac) {
|
|
@@ -50,11 +51,11 @@ class OdacRequest {
|
|
|
50
51
|
this.status(code)
|
|
51
52
|
let result = {401: 'Unauthorized', 404: 'Not Found', 408: 'Request Timeout'}[code] ?? null
|
|
52
53
|
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
typeof
|
|
54
|
+
this.#odac.Route?.routes?.[this.route]?.error &&
|
|
55
|
+
this.#odac.Route.routes[this.route].error[code] &&
|
|
56
|
+
typeof this.#odac.Route.routes[this.route].error[code].cache === 'function'
|
|
56
57
|
)
|
|
57
|
-
result = await
|
|
58
|
+
result = await this.#odac.Route.routes[this.route].error[code].cache(this.#odac)
|
|
58
59
|
this.end(result)
|
|
59
60
|
}
|
|
60
61
|
|
package/src/Route.js
CHANGED
|
@@ -150,6 +150,17 @@ class Route {
|
|
|
150
150
|
}
|
|
151
151
|
Odac.Request.isAjaxLoad = true
|
|
152
152
|
Odac.Request.clientSkeleton = Odac.Request.header('X-Odac-Skeleton')
|
|
153
|
+
|
|
154
|
+
// Parse client's current part values for smart diffing
|
|
155
|
+
const partsHeader = Odac.Request.header('X-Odac-Parts')
|
|
156
|
+
if (partsHeader) {
|
|
157
|
+
const parts = {}
|
|
158
|
+
for (const entry of partsHeader.split(',')) {
|
|
159
|
+
const idx = entry.indexOf('=')
|
|
160
|
+
if (idx > 0) parts[entry.substring(0, idx)] = decodeURIComponent(entry.substring(idx + 1))
|
|
161
|
+
}
|
|
162
|
+
Odac.Request.clientParts = parts
|
|
163
|
+
}
|
|
153
164
|
}
|
|
154
165
|
if (Odac.Config?.route?.[url]) {
|
|
155
166
|
// PROD CACHE HIT
|