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
package/.agent/rules/memory.md
CHANGED
|
@@ -32,12 +32,14 @@ trigger: always_on
|
|
|
32
32
|
|
|
33
33
|
## Dependency Management
|
|
34
34
|
- **Prefer Native Fetch:** Use the native `fetch` API for network requests in both Node.js (18+) and browser environments to reduce dependencies and bundle size.
|
|
35
|
+
- **Hybrid Overrides:** When using `overrides` in `package.json` for security hardening, always verify tool compatibility (especially Jest/Coverage). If a global override (e.g., `minimatch: 10.x`) breaks a critical utility (e.g., `test-exclude`), use a nested override to provide a compatible version (e.g., `3.x`) for that specific tool while keeping the rest of the project secure.
|
|
35
36
|
|
|
36
37
|
## Naming & Text Conventions
|
|
37
38
|
- **ODAC Casing:** Always write "ODAC" in uppercase letters when referring to the framework name in strings, comments, log messages, or user-facing text. **EXCEPTION:** The class name itself (`class Odac`) and variable references to it should remain `Odac` (PascalCase) as per code conventions.
|
|
38
39
|
|
|
39
40
|
## Documentation Standards
|
|
40
41
|
- **AI Skill Front Matter:** Every file under `docs/ai/skills/**/*.md` must start with YAML front matter containing `name`, `description`, and `metadata.tags`; values must be specific to that document's topic (never copied from generic examples).
|
|
42
|
+
- **Template Syntax Documentation:** `{{ }}` and `<odac var>` are equal-status syntaxes, NOT legacy vs modern. `{{ }}` is preferred inside HTML attributes and inline text; `<odac var>` is preferred for standalone block output. Never label `{{ }}` as "legacy" or "backward compatibility" in docs.
|
|
41
43
|
|
|
42
44
|
## Testing & Validation
|
|
43
45
|
- **Mandatory Test Coverage:** Every new feature, method, or significant logic change MUST be accompanied by a corresponding unit or integration test.
|
|
@@ -53,4 +55,11 @@ trigger: always_on
|
|
|
53
55
|
- **Automatic JSON Parsing:** The `#ajax` method (and by extension `odac.get`) must automatically parse the response if the `Content-Type` header contains `application/json`, even if `dataType` is not explicitly set to `json`.
|
|
54
56
|
|
|
55
57
|
## Security Logic & Authentication
|
|
56
|
-
- **Enterprise Token Rotation:** The `Auth.js` system utilizes a non-blocking refresh token rotation mechanism for cookies (`odac_x`/`odac_y`). To prevent race conditions during concurrent requests in high-throughput SPAs, rotated tokens are **not** immediately deleted. Instead, their `active` timestamp is set to naturally expire in 60 seconds (Grace Period), and their `date` timestamp is set to the Unix Epoch (`new Date(0)`) as an identifier mark. Never delete rotated tokens immediately.
|
|
58
|
+
- **Enterprise Token Rotation:** The `Auth.js` system utilizes a non-blocking refresh token rotation mechanism for cookies (`odac_x`/`odac_y`). To prevent race conditions during concurrent requests in high-throughput SPAs, rotated tokens are **not** immediately deleted. Instead, their `active` timestamp is set to naturally expire in 60 seconds (Grace Period), and their `date` timestamp is set to the Unix Epoch (`new Date(0)`) as an identifier mark. Never delete rotated tokens immediately.
|
|
59
|
+
- **Template String Escaping:** When escaping template strings for inline JavaScript execution or translation payloads, always escape backslashes first before escaping single quotes (e.g., `.replace(/\\/g, '\\\\').replace(/'/g, "\\'")`) to prevent an injected backslash from disabling the quote escape and causing a Template Injection / XSS vulnerability.
|
|
60
|
+
|
|
61
|
+
## View Engine & SSR
|
|
62
|
+
- **Auto-Navigation Injection:** The ODAC View engine (`src/View.js`) automatically parses skeleton HTML files and dynamically injects `data-odac-navigate` attributes into ALL elements wrapping `{{ PART_NAME }}` placeholders (e.g. `{{ CONTENT }}`, `{{ SIDEBAR }}`, `{{ FOOTER }}`). Do NOT manually add these attributes to HTML templates.
|
|
63
|
+
- **Smart Part Diffing (AJAX Navigation):** The AJAX navigation system uses a server-driven part diffing mechanism. The client sends its current part values via `X-Odac-Parts` header (e.g. `content=docs/intro,sidebar=docs/nav`). The server compares these with the new page's parts and only renders/returns parts whose view path has changed. Parts that are identical between pages are skipped (no re-render, no DOM update). Parts that exist on the old page but not the new page are cleared. The `parts` manifest is included in every AJAX response for the client to track state. **Exception:** The `content` part is always re-rendered regardless of view path match, since its output is URL-dependent.
|
|
64
|
+
- **Part Refresh Override:** `View.set('partName', 'viewPath', { refresh: true })` forces a part to re-render on every AJAX navigation even if its view path hasn't changed. Useful for parts with request-dependent dynamic content (e.g. active menu state). The `#refresh` Set tracks which parts bypass the diffing comparison.
|
|
65
|
+
- **Initial Parts State:** On first page load, the `<html>` tag receives a `data-odac-parts` JSON attribute containing the current part-to-view mapping. The client reads this to initialize its parts tracking state for subsequent AJAX navigations.
|
package/.releaserc.js
CHANGED
|
@@ -135,7 +135,7 @@ Powered by [⚡ ODAC](https://odac.run)
|
|
|
135
135
|
[
|
|
136
136
|
'@semantic-release/git',
|
|
137
137
|
{
|
|
138
|
-
assets: ['package.json', 'CHANGELOG.md'],
|
|
138
|
+
assets: ['package.json', 'package-lock.json', 'CHANGELOG.md'],
|
|
139
139
|
message: '⚡ ODAC.JS v${nextRelease.version} Released'
|
|
140
140
|
}
|
|
141
141
|
],
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
+
### ⚙️ Engine Tuning
|
|
2
|
+
|
|
3
|
+
- replace global Odac reference with private instance and update dependency overrides for security hardening
|
|
4
|
+
- **test/view:** remove unused CACHE_DIR variable in parseOdacTag tests
|
|
5
|
+
|
|
6
|
+
### ✨ What's New
|
|
7
|
+
|
|
8
|
+
- **client:** log malformed data-odac-parts JSON to ease debugging
|
|
9
|
+
- **view:** smart AJAX part diffing with selective re-render
|
|
10
|
+
|
|
11
|
+
### 🛠️ Fixes & Improvements
|
|
12
|
+
|
|
13
|
+
- **route:** encode X-Odac-Parts header values to prevent splitting errors
|
|
14
|
+
- **View:** escape single quotes in template expressions to prevent syntax errors
|
|
15
|
+
- **view:** prevent template injection by escaping backslashes in dynamic parser
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
22
|
+
|
|
23
|
+
### security
|
|
24
|
+
|
|
25
|
+
- **template:** add noopener to external footer links to mitigate reverse tabnabbing
|
|
26
|
+
- **template:** add rel="noopener" to all external links to prevent tabnabbing without losing referer analytics
|
|
27
|
+
|
|
28
|
+
### ✨ What's New
|
|
29
|
+
|
|
30
|
+
- **client:** add native View Transition API support via odac-transition attribute
|
|
31
|
+
- Implement a complete UI redesign using Tailwind CSS, introduce new components, and update branding to ODAC.
|
|
32
|
+
|
|
33
|
+
### 📚 Documentation
|
|
34
|
+
|
|
35
|
+
- Add documentation for auto-navigation injection in the View Engine and SSR.
|
|
36
|
+
- **template:** correct casing for Odac object api references in comments
|
|
37
|
+
- **views:** clarify template syntax as equal-status with usage guidelines
|
|
38
|
+
|
|
39
|
+
### 🛠️ Fixes & Improvements
|
|
40
|
+
|
|
41
|
+
- **client:** clear stale view transition names on aborted navigation promises
|
|
42
|
+
- **client:** decode HTML entities in document title to prevent XSS vulnerabilities
|
|
43
|
+
- **template:** sync active nav state properly across desktop and mobile menus
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
50
|
+
|
|
1
51
|
### doc
|
|
2
52
|
|
|
3
53
|
- Introduce WebSocket routing and controllers, update request handling, and refactor language and validator modules to use async operations.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 🚀 **Developer Friendly:** Simple setup and intuitive API design let you start building immediately.
|
|
9
9
|
* 🎨 **Built-in Tailwind CSS:** Zero-config integration with Tailwind CSS v4. Automatic compilation and optimization out of the box.
|
|
10
10
|
* 🔗 **Powerful Routing:** Create clean, custom URLs and manage infinite pages with a flexible routing system.
|
|
11
|
-
* ✨ **Seamless SPA Experience:** Automatic AJAX handling for forms and page transitions
|
|
11
|
+
* ✨ **Seamless SPA Experience:** Automatic AJAX handling for forms and page transitions with native **View Transition API** support. Add a single HTML attribute for smooth, browser-native animations — no client-side code required.
|
|
12
12
|
* 🛡️ **Built-in Security:** Enterprise-grade security out of the box. Includes secure default headers and a **Multi-tab Safe, Single-Use CSRF Protection (Nonce)**. Tokens self-replenish in the background, ensuring maximum defense without ever interrupting the user experience.
|
|
13
13
|
* 🔐 **Authentication:** Ready-to-use session management with enterprise-grade **Refresh Token Rotation**, secure password hashing, and authentication helpers.
|
|
14
14
|
* 🗄️ **Database Agnostic:** Integrated support for major databases (PostgreSQL, MySQL, SQLite) and Redis via Knex.js.
|
|
@@ -72,6 +72,7 @@ project/
|
|
|
72
72
|
├── middleware/ # Route middlewares
|
|
73
73
|
├── public/ # Static assets
|
|
74
74
|
├── route/ # Route definitions
|
|
75
|
+
├── schema/ # Database schemas (auto-migrate)
|
|
75
76
|
├── view/ # HTML templates
|
|
76
77
|
├── .env # Environment variables
|
|
77
78
|
└── odac.json # App configuration
|
package/client/odac.js
CHANGED
|
@@ -119,7 +119,7 @@ if (typeof window !== 'undefined') {
|
|
|
119
119
|
#page = null
|
|
120
120
|
#token = {hash: [], data: false}
|
|
121
121
|
#formSubmitHandlers = new Map()
|
|
122
|
-
#loader = {elements: {}, callback: null}
|
|
122
|
+
#loader = {elements: {}, callback: null, parts: {}}
|
|
123
123
|
#isNavigating = false
|
|
124
124
|
|
|
125
125
|
constructor() {
|
|
@@ -195,6 +195,33 @@ if (typeof window !== 'undefined') {
|
|
|
195
195
|
xhr.send(data)
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Assigns `view-transition-name` CSS properties to all elements carrying
|
|
200
|
+
* the `odac-transition` attribute, enabling the browser's native View
|
|
201
|
+
* Transition API to animate them individually during navigation.
|
|
202
|
+
*
|
|
203
|
+
* @returns {Element[]} The list of elements that received transition names.
|
|
204
|
+
*/
|
|
205
|
+
#applyTransitionNames() {
|
|
206
|
+
const elements = document.querySelectorAll('[odac-transition]')
|
|
207
|
+
elements.forEach(el => {
|
|
208
|
+
el.style.viewTransitionName = el.getAttribute('odac-transition')
|
|
209
|
+
})
|
|
210
|
+
return Array.from(elements)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Removes `view-transition-name` from previously tagged elements to
|
|
215
|
+
* prevent stale names from conflicting with future transitions.
|
|
216
|
+
*
|
|
217
|
+
* @param {Element[]} elements - Elements to clean up.
|
|
218
|
+
*/
|
|
219
|
+
#clearTransitionNames(elements) {
|
|
220
|
+
elements.forEach(el => {
|
|
221
|
+
el.style.viewTransitionName = ''
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
198
225
|
#fade(element, type, duration = 400, callback) {
|
|
199
226
|
const isIn = type === 'in'
|
|
200
227
|
const startOpacity = isIn ? 0 : 1
|
|
@@ -720,6 +747,20 @@ if (typeof window !== 'undefined') {
|
|
|
720
747
|
.replace(/\n/g, '<br>')
|
|
721
748
|
}
|
|
722
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Decodes HTML entities back to their plain-text representation.
|
|
752
|
+
* Uses a textarea element as a safe decoder — no script execution risk.
|
|
753
|
+
*
|
|
754
|
+
* @param {string} str - HTML-encoded string (e.g. "&" → "&").
|
|
755
|
+
* @returns {string} Decoded plain-text string.
|
|
756
|
+
*/
|
|
757
|
+
#decodeHtmlEntities(str) {
|
|
758
|
+
if (typeof str !== 'string') return str
|
|
759
|
+
const textarea = document.createElement('textarea')
|
|
760
|
+
textarea.innerHTML = str
|
|
761
|
+
return textarea.value
|
|
762
|
+
}
|
|
763
|
+
|
|
723
764
|
load(url, callback, push = true) {
|
|
724
765
|
if (this.#isNavigating) return false
|
|
725
766
|
|
|
@@ -731,82 +772,188 @@ if (typeof window !== 'undefined') {
|
|
|
731
772
|
this.#isNavigating = true
|
|
732
773
|
|
|
733
774
|
const currentSkeleton = document.documentElement.dataset.odacSkeleton
|
|
734
|
-
const elements = Object.entries(this.#loader.elements)
|
|
735
775
|
|
|
736
|
-
|
|
737
|
-
elements
|
|
738
|
-
|
|
739
|
-
|
|
776
|
+
// Merge loader-registered elements with auto-discovered navigate elements
|
|
777
|
+
const elements = {...this.#loader.elements}
|
|
778
|
+
document.querySelectorAll('[data-odac-navigate]').forEach(el => {
|
|
779
|
+
const partName = el.getAttribute('data-odac-navigate')
|
|
780
|
+
if (!elements[partName]) elements[partName] = `[data-odac-navigate="${partName}"]`
|
|
740
781
|
})
|
|
741
782
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
783
|
+
const elementsToUpdate = []
|
|
784
|
+
for (const [key, selector] of Object.entries(elements)) {
|
|
785
|
+
const element = document.querySelector(selector)
|
|
786
|
+
if (element) elementsToUpdate.push({key, element, selector})
|
|
787
|
+
}
|
|
746
788
|
|
|
747
|
-
|
|
748
|
-
|
|
789
|
+
// Build X-Odac-Parts header from current known parts
|
|
790
|
+
const partsHeader = Object.entries(this.#loader.parts)
|
|
791
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
792
|
+
.join(',')
|
|
749
793
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
window.location.href = finalUrl
|
|
753
|
-
return
|
|
754
|
-
}
|
|
755
|
-
if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
|
|
794
|
+
// Collect part keys to request from server
|
|
795
|
+
const loadKeys = Object.keys(elements)
|
|
756
796
|
|
|
757
|
-
|
|
758
|
-
if (newPage !== null) {
|
|
759
|
-
this.#page = newPage
|
|
760
|
-
document.documentElement.dataset.odacPage = newPage
|
|
761
|
-
}
|
|
797
|
+
const useViewTransition = document.startViewTransition && document.querySelectorAll('[odac-transition]').length > 0
|
|
762
798
|
|
|
763
|
-
|
|
764
|
-
|
|
799
|
+
if (useViewTransition) {
|
|
800
|
+
this.#loadWithViewTransition(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate, partsHeader, loadKeys)
|
|
801
|
+
} else {
|
|
802
|
+
this.#loadWithFade(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate, partsHeader, loadKeys)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
765
805
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
806
|
+
/**
|
|
807
|
+
* Performs page navigation using the browser's native View Transition API.
|
|
808
|
+
* Elements with `odac-transition` attributes receive individual transition
|
|
809
|
+
* names, enabling per-element morphing animations orchestrated by the browser.
|
|
810
|
+
* Non-transition elements still update their content within the transition frame.
|
|
811
|
+
*/
|
|
812
|
+
#loadWithViewTransition(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate, partsHeader, loadKeys) {
|
|
813
|
+
const oldTransitionElements = this.#applyTransitionNames()
|
|
770
814
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
completed++
|
|
776
|
-
if (completed === elementsToUpdate.length) this.#handleLoadComplete(ajaxData, callback)
|
|
777
|
-
})
|
|
778
|
-
})
|
|
815
|
+
const headers = {
|
|
816
|
+
'X-Odac': 'ajaxload',
|
|
817
|
+
'X-Odac-Load': loadKeys.join(','),
|
|
818
|
+
'X-Odac-Skeleton': currentSkeleton || ''
|
|
779
819
|
}
|
|
820
|
+
if (partsHeader) headers['X-Odac-Parts'] = partsHeader
|
|
821
|
+
|
|
822
|
+
this.#ajax({
|
|
823
|
+
url,
|
|
824
|
+
type: 'GET',
|
|
825
|
+
headers,
|
|
826
|
+
dataType: 'json',
|
|
827
|
+
success: (data, status, xhr) => {
|
|
828
|
+
const finalUrl = xhr.responseURL || url
|
|
829
|
+
if (data.skeletonChanged) {
|
|
830
|
+
this.#clearTransitionNames(oldTransitionElements)
|
|
831
|
+
window.location.href = finalUrl
|
|
832
|
+
return
|
|
833
|
+
}
|
|
780
834
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
835
|
+
// Update tracked parts from server manifest
|
|
836
|
+
if (data.parts) this.#loader.parts = data.parts
|
|
837
|
+
|
|
838
|
+
// Determine which parts were removed by the new page
|
|
839
|
+
const removedParts = []
|
|
840
|
+
for (const {key, element} of elementsToUpdate) {
|
|
841
|
+
if (data.parts && !(key in data.parts)) removedParts.push({key, element})
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const transition = document.startViewTransition(() => {
|
|
845
|
+
if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
|
|
846
|
+
|
|
847
|
+
const newPage = xhr.getResponseHeader('X-Odac-Page')
|
|
848
|
+
if (newPage !== null) {
|
|
849
|
+
this.#page = newPage
|
|
850
|
+
document.documentElement.dataset.odacPage = newPage
|
|
789
851
|
}
|
|
852
|
+
|
|
853
|
+
if (data.data) this.#data = data.data
|
|
854
|
+
if (data.title) document.title = this.#decodeHtmlEntities(data.title)
|
|
855
|
+
|
|
856
|
+
// Update only parts that have new content from server
|
|
857
|
+
elementsToUpdate.forEach(({key, element}) => {
|
|
858
|
+
if (data.output && data.output[key] !== undefined) element.innerHTML = data.output[key]
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
// Clear removed parts
|
|
862
|
+
for (const {element} of removedParts) {
|
|
863
|
+
element.innerHTML = ''
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
this.#applyTransitionNames()
|
|
790
867
|
})
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
868
|
+
|
|
869
|
+
transition.finished
|
|
870
|
+
.then(() => {
|
|
871
|
+
this.#clearTransitionNames(document.querySelectorAll('[odac-transition]'))
|
|
872
|
+
this.#handleLoadComplete(data, callback)
|
|
873
|
+
})
|
|
874
|
+
.catch(() => {
|
|
875
|
+
this.#clearTransitionNames(document.querySelectorAll('[odac-transition]'))
|
|
876
|
+
this.#isNavigating = false
|
|
877
|
+
})
|
|
878
|
+
},
|
|
879
|
+
error: () => {
|
|
880
|
+
this.#clearTransitionNames(oldTransitionElements)
|
|
881
|
+
this.#isNavigating = false
|
|
882
|
+
window.location.replace(url)
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Performs page navigation using the legacy fade-in/fade-out animation.
|
|
889
|
+
* Only fades elements whose content actually changed, leaving unchanged
|
|
890
|
+
* parts (like a shared sidebar) completely untouched and visible.
|
|
891
|
+
*/
|
|
892
|
+
#loadWithFade(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate, partsHeader, loadKeys) {
|
|
893
|
+
const headers = {
|
|
894
|
+
'X-Odac': 'ajaxload',
|
|
895
|
+
'X-Odac-Load': loadKeys.join(','),
|
|
896
|
+
'X-Odac-Skeleton': currentSkeleton || ''
|
|
794
897
|
}
|
|
898
|
+
if (partsHeader) headers['X-Odac-Parts'] = partsHeader
|
|
795
899
|
|
|
796
900
|
this.#ajax({
|
|
797
|
-
url
|
|
901
|
+
url,
|
|
798
902
|
type: 'GET',
|
|
799
|
-
headers
|
|
800
|
-
'X-Odac': 'ajaxload',
|
|
801
|
-
'X-Odac-Load': Object.keys(this.#loader.elements).join(','),
|
|
802
|
-
'X-Odac-Skeleton': currentSkeleton || ''
|
|
803
|
-
},
|
|
903
|
+
headers,
|
|
804
904
|
dataType: 'json',
|
|
805
905
|
success: (data, status, xhr) => {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
906
|
+
const finalUrl = xhr.responseURL || url
|
|
907
|
+
if (data.skeletonChanged) {
|
|
908
|
+
window.location.href = finalUrl
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
|
|
912
|
+
|
|
913
|
+
const newPage = xhr.getResponseHeader('X-Odac-Page')
|
|
914
|
+
if (newPage !== null) {
|
|
915
|
+
this.#page = newPage
|
|
916
|
+
document.documentElement.dataset.odacPage = newPage
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (data.data) this.#data = data.data
|
|
920
|
+
if (data.title) document.title = this.#decodeHtmlEntities(data.title)
|
|
921
|
+
if (data.parts) this.#loader.parts = data.parts
|
|
922
|
+
|
|
923
|
+
// Classify elements: changed, removed, or unchanged
|
|
924
|
+
const changedParts = []
|
|
925
|
+
const removedParts = []
|
|
926
|
+
|
|
927
|
+
for (const {key, element} of elementsToUpdate) {
|
|
928
|
+
if (data.output && data.output[key] !== undefined) {
|
|
929
|
+
changedParts.push({key, element})
|
|
930
|
+
} else if (data.parts && !(key in data.parts)) {
|
|
931
|
+
removedParts.push({key, element})
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Clear removed parts immediately
|
|
936
|
+
for (const {element} of removedParts) {
|
|
937
|
+
element.innerHTML = ''
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// No changed parts — complete immediately
|
|
941
|
+
if (changedParts.length === 0) {
|
|
942
|
+
this.#handleLoadComplete(data, callback)
|
|
943
|
+
return
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Fade out only changed parts, then swap content and fade in
|
|
947
|
+
let fadeOutCount = 0
|
|
948
|
+
changedParts.forEach(({key, element}) => {
|
|
949
|
+
this.#fadeOut(element, 200, () => {
|
|
950
|
+
element.innerHTML = data.output[key]
|
|
951
|
+
this.#fadeIn(element, 200, () => {
|
|
952
|
+
fadeOutCount++
|
|
953
|
+
if (fadeOutCount === changedParts.length) this.#handleLoadComplete(data, callback)
|
|
954
|
+
})
|
|
955
|
+
})
|
|
956
|
+
})
|
|
810
957
|
},
|
|
811
958
|
error: () => {
|
|
812
959
|
this.#isNavigating = false
|
|
@@ -855,9 +1002,10 @@ if (typeof window !== 'undefined') {
|
|
|
855
1002
|
}
|
|
856
1003
|
}
|
|
857
1004
|
|
|
858
|
-
loader(selector, elements, callback) {
|
|
1005
|
+
loader(selector, elements, callback, initialParts) {
|
|
859
1006
|
this.#loader.elements = elements
|
|
860
1007
|
this.#loader.callback = callback
|
|
1008
|
+
if (initialParts) this.#loader.parts = initialParts
|
|
861
1009
|
const odacInstance = this
|
|
862
1010
|
|
|
863
1011
|
this.#on(document, 'click', selector, function (e) {
|
|
@@ -1033,10 +1181,28 @@ if (typeof window !== 'undefined') {
|
|
|
1033
1181
|
window.Odac = new _odac()
|
|
1034
1182
|
;(function initAutoNavigate() {
|
|
1035
1183
|
const init = () => {
|
|
1036
|
-
const
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1184
|
+
const navigateElements = document.querySelectorAll('[data-odac-navigate]')
|
|
1185
|
+
if (navigateElements.length === 0) return
|
|
1186
|
+
|
|
1187
|
+
const elements = {}
|
|
1188
|
+
navigateElements.forEach(el => {
|
|
1189
|
+
const partName = el.getAttribute('data-odac-navigate')
|
|
1190
|
+
elements[partName] = `[data-odac-navigate="${partName}"]`
|
|
1191
|
+
})
|
|
1192
|
+
|
|
1193
|
+
// Initialize parts state from server-rendered data attribute
|
|
1194
|
+
const partsAttr = document.documentElement.dataset.odacParts
|
|
1195
|
+
let initialParts = {}
|
|
1196
|
+
if (partsAttr) {
|
|
1197
|
+
try {
|
|
1198
|
+
initialParts = JSON.parse(partsAttr)
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
console.error('Odac: Failed to parse data-odac-parts attribute.', e)
|
|
1201
|
+
// Graceful fallback if attribute is malformed
|
|
1202
|
+
}
|
|
1039
1203
|
}
|
|
1204
|
+
|
|
1205
|
+
window.Odac.loader('a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)', elements, null, initialParts)
|
|
1040
1206
|
}
|
|
1041
1207
|
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()
|
|
1042
1208
|
})()
|
|
@@ -1,61 +1,133 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: backend-views-templates-skill
|
|
3
|
-
description: ODAC server-side rendering guidelines for
|
|
3
|
+
description: ODAC server-side rendering guidelines for skeleton layouts, smart part diffing, template syntax, and safe output rendering.
|
|
4
4
|
metadata:
|
|
5
|
-
tags: backend, views, templates, ssr, xss-protection, skeleton, rendering
|
|
5
|
+
tags: backend, views, templates, ssr, xss-protection, skeleton, rendering, part-diffing, ajax-navigation
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Backend Views & Templates Skill
|
|
9
9
|
|
|
10
|
-
High-performance server-side rendering using ODAC's optimized template engine.
|
|
10
|
+
High-performance server-side rendering using ODAC's optimized template engine with smart AJAX part diffing.
|
|
11
11
|
|
|
12
12
|
## Architectural Approach
|
|
13
|
-
Views in ODAC are logic-light but powerful. They support automatic XSS protection, high-performance looping,
|
|
13
|
+
Views in ODAC are logic-light but powerful. They support automatic XSS protection, high-performance looping, server-side JavaScript execution via `<script:odac>`, and a smart AJAX navigation system that only updates parts of the page that actually changed.
|
|
14
14
|
|
|
15
15
|
## Core Rules
|
|
16
16
|
1. **Skeleton Architecture**: Use `Odac.View.skeleton('name')` to wrap content in a layout.
|
|
17
|
-
2. **
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
2. **Part Setting**: Use `Odac.View.set(partName, viewPath)` to fill skeleton placeholders.
|
|
18
|
+
3. **Data Binding — Two Equivalent Syntaxes**:
|
|
19
|
+
- `<odac var="key" />`: Tag-based output (HTML-escaped, XSS-safe).
|
|
20
|
+
- `{{ key }}`: Inline/interpolation output (HTML-escaped, XSS-safe). Identical behavior to `<odac var>`.
|
|
21
|
+
- `<odac var="key" raw />` or `{!! key !!}`: Raw output (use with extreme caution).
|
|
22
|
+
4. **Choosing the Right Syntax**:
|
|
23
|
+
- **Inside HTML attributes** (`src`, `alt`, `href`, `class`, `value`, etc.) → Always prefer `{{ }}`.
|
|
24
|
+
- **Inline within text or mixed HTML** → Prefer `{{ }}` for short interpolations.
|
|
25
|
+
- **Standalone block output** → Prefer `<odac var="" />` for structural clarity.
|
|
26
|
+
5. **Conditionals**: Use `<odac:if condition="VAR"> ... </odac:if>`.
|
|
27
|
+
6. **Looping**: Use `<odac:for in="ARRAY" value="ITEM"> ... </odac:for>`.
|
|
28
|
+
7. **Server-Side JS**: Use `<script:odac>` for complex calculations during rendering.
|
|
29
|
+
|
|
30
|
+
## Smart Part Diffing (AJAX Navigation)
|
|
31
|
+
|
|
32
|
+
ODAC uses a **server-driven part diffing** system during AJAX navigation. Understanding this is critical for building multi-section layouts correctly.
|
|
33
|
+
|
|
34
|
+
### How it works
|
|
35
|
+
- The client tracks which view file each part is currently showing.
|
|
36
|
+
- On every AJAX navigation, it sends this state to the server via `X-Odac-Parts`.
|
|
37
|
+
- The server compares the new page's parts against the client's current state and **only renders parts that changed**.
|
|
38
|
+
- The client only updates DOM elements that received new content.
|
|
39
|
+
|
|
40
|
+
### Rules
|
|
41
|
+
| Scenario | Behavior |
|
|
42
|
+
|----------|----------|
|
|
43
|
+
| Part view path unchanged | Skipped — not re-rendered, DOM untouched |
|
|
44
|
+
| Part view path changed | Re-rendered and DOM updated |
|
|
45
|
+
| Part removed on new page | DOM element content cleared |
|
|
46
|
+
| Skeleton changed | Full page reload |
|
|
47
|
+
| `content` part | **Always** re-rendered (URL-dependent by nature) |
|
|
48
|
+
|
|
49
|
+
### Force-refresh a part
|
|
50
|
+
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):
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
// Always re-renders on AJAX navigation even if view path is unchanged
|
|
54
|
+
Odac.View.set('sidebar', 'docs.nav', { refresh: true })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Skeleton placeholder rules
|
|
58
|
+
- Each `{{ PLACEHOLDER }}` must be wrapped in its own HTML element.
|
|
59
|
+
- The engine auto-injects `data-odac-navigate` on wrapper elements — do not add manually.
|
|
60
|
+
- Unset placeholders are silently removed from the final HTML output.
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<!-- skeleton/main.html -->
|
|
64
|
+
<aside>{{ SIDEBAR }}</aside> <!-- auto-gets data-odac-navigate="sidebar" -->
|
|
65
|
+
<main>{{ CONTENT }}</main> <!-- auto-gets data-odac-navigate="content" -->
|
|
66
|
+
```
|
|
23
67
|
|
|
24
68
|
## Reference Patterns
|
|
25
69
|
|
|
26
|
-
### 1.
|
|
70
|
+
### 1. Standard Page with Shared Layout
|
|
71
|
+
```javascript
|
|
72
|
+
// controller/docs/page.js
|
|
73
|
+
module.exports = function (Odac) {
|
|
74
|
+
Odac.View
|
|
75
|
+
.skeleton('main')
|
|
76
|
+
.set('sidebar', 'docs.nav') // shared — skipped if unchanged on next navigate
|
|
77
|
+
.set('content', 'docs.intro') // always re-rendered
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Sidebar with Active State (force refresh)
|
|
27
82
|
```javascript
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
Odac.View
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
// controller/docs/page.js
|
|
84
|
+
module.exports = function (Odac) {
|
|
85
|
+
Odac.View
|
|
86
|
+
.skeleton('main')
|
|
87
|
+
.set('sidebar', 'docs.nav', { refresh: true }) // re-renders every navigate
|
|
88
|
+
.set('content', 'docs.intro')
|
|
89
|
+
}
|
|
34
90
|
```
|
|
35
91
|
|
|
36
|
-
###
|
|
92
|
+
### 3. Page Without Sidebar
|
|
93
|
+
```javascript
|
|
94
|
+
// controller/home.js — navigating here clears the sidebar DOM element
|
|
95
|
+
module.exports = function (Odac) {
|
|
96
|
+
Odac.View
|
|
97
|
+
.skeleton('main')
|
|
98
|
+
.set('content', 'home')
|
|
99
|
+
// sidebar not set → its DOM element is emptied on AJAX navigation
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Template Syntax Reference
|
|
37
104
|
```html
|
|
38
|
-
<!--
|
|
39
|
-
<h1
|
|
105
|
+
<!-- Standalone block output — prefer <odac var> -->
|
|
106
|
+
<h1><odac var="title" /></h1>
|
|
107
|
+
|
|
108
|
+
<!-- Inside attributes — prefer {{ }} -->
|
|
109
|
+
<img src="{{ product.image }}" alt="{{ product.name }}">
|
|
110
|
+
<a href="/user/{{ user.id }}" class="btn {{ isActive ? 'active' : '' }}">Profile</a>
|
|
111
|
+
|
|
112
|
+
<!-- Inline text interpolation -->
|
|
113
|
+
<p>Welcome, {{ user.name }}. You have {{ notifications }} new messages.</p>
|
|
40
114
|
|
|
41
115
|
<!-- Conditional -->
|
|
42
116
|
<odac:if condition="stats.users > 100">
|
|
43
117
|
<span class="badge">Popular!</span>
|
|
44
118
|
</odac:if>
|
|
45
119
|
|
|
46
|
-
<!--
|
|
47
|
-
|
|
120
|
+
<!-- Loop -->
|
|
121
|
+
<odac:for in="users" value="user">
|
|
48
122
|
<li>{{ user.name }}</li>
|
|
49
|
-
|
|
123
|
+
</odac:for>
|
|
50
124
|
```
|
|
51
125
|
|
|
52
|
-
###
|
|
53
|
-
Perfect for calculations that shouldn't clutter the controller but are too complex for simple tags.
|
|
126
|
+
### 5. Backend JavaScript (`<script:odac>`)
|
|
54
127
|
```html
|
|
55
128
|
<script:odac>
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const tax = total * 0.18;
|
|
129
|
+
const total = items.reduce((sum, i) => sum + i.price, 0)
|
|
130
|
+
const tax = total * 0.18
|
|
59
131
|
</script:odac>
|
|
60
132
|
|
|
61
133
|
<p>Subtotal: ${{ total }}</p>
|
|
@@ -63,6 +135,6 @@ Perfect for calculations that shouldn't clutter the controller but are too compl
|
|
|
63
135
|
```
|
|
64
136
|
|
|
65
137
|
## Security Best Practices
|
|
66
|
-
- **
|
|
138
|
+
- **Both `{{ }}` and `<odac var>` are XSS-safe**: Both apply HTML escaping by default.
|
|
139
|
+
- **Raw output requires trust**: Only use `raw` / `{!! !!}` with content you fully control. Never with user input.
|
|
67
140
|
- **Limit `<script:odac>`**: Do not perform database queries or API calls inside views; keep them in the controller.
|
|
68
|
-
- **Partial Awareness**: Use `<odac:include view="path.to.view" />` for reusable components.
|