odac 1.4.5 → 1.4.7

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.
@@ -32,6 +32,7 @@ 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.
@@ -55,6 +56,10 @@ trigger: always_on
55
56
 
56
57
  ## Security Logic & Authentication
57
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.
58
60
 
59
61
  ## View Engine & SSR
60
- - **Auto-Navigation Injection:** The ODAC View engine (`src/View.js`) automatically parses skeleton HTML files and dynamically injects `data-odac-navigate="content"` into the element immediately wrapping `{{ CONTENT }}`. Do NOT manually add this attribute to HTML templates or assume it is missing based on static analysis, as it is handled seamlessly at runtime via 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,45 @@
1
+ ### ⚙️ Engine Tuning
2
+
3
+ - **test:** remove unused fsPromises and standardize async I/O in View tests
4
+
5
+ ### 📚 Documentation
6
+
7
+ - add quick start guide and register in documentation index
8
+
9
+ ### 🛠️ Fixes & Improvements
10
+
11
+ - add raw attribute support to `<odac get>` tag for unescaped HTML output
12
+ - improve title extraction logic and optimize data-odac-navigate attribute injection in View rendering
13
+ - prevent data-odac-navigate injection into closing HTML tags
14
+
15
+
16
+
17
+ ---
18
+
19
+ Powered by [⚡ ODAC](https://odac.run)
20
+
21
+ ### ⚙️ Engine Tuning
22
+
23
+ - replace global Odac reference with private instance and update dependency overrides for security hardening
24
+ - **test/view:** remove unused CACHE_DIR variable in parseOdacTag tests
25
+
26
+ ### ✨ What's New
27
+
28
+ - **client:** log malformed data-odac-parts JSON to ease debugging
29
+ - **view:** smart AJAX part diffing with selective re-render
30
+
31
+ ### 🛠️ Fixes & Improvements
32
+
33
+ - **route:** encode X-Odac-Parts header values to prevent splitting errors
34
+ - **View:** escape single quotes in template expressions to prevent syntax errors
35
+ - **view:** prevent template injection by escaping backslashes in dynamic parser
36
+
37
+
38
+
39
+ ---
40
+
41
+ Powered by [⚡ ODAC](https://odac.run)
42
+
1
43
  ### security
2
44
 
3
45
  - **template:** add noopener to external footer links to mitigate reverse tabnabbing
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() {
@@ -772,20 +772,34 @@ if (typeof window !== 'undefined') {
772
772
  this.#isNavigating = true
773
773
 
774
774
  const currentSkeleton = document.documentElement.dataset.odacSkeleton
775
- const elements = Object.entries(this.#loader.elements)
775
+
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}"]`
781
+ })
776
782
 
777
783
  const elementsToUpdate = []
778
- elements.forEach(([key, selector]) => {
784
+ for (const [key, selector] of Object.entries(elements)) {
779
785
  const element = document.querySelector(selector)
780
- if (element) elementsToUpdate.push({key, element})
781
- })
786
+ if (element) elementsToUpdate.push({key, element, selector})
787
+ }
788
+
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(',')
793
+
794
+ // Collect part keys to request from server
795
+ const loadKeys = Object.keys(elements)
782
796
 
783
797
  const useViewTransition = document.startViewTransition && document.querySelectorAll('[odac-transition]').length > 0
784
798
 
785
799
  if (useViewTransition) {
786
- this.#loadWithViewTransition(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate)
800
+ this.#loadWithViewTransition(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate, partsHeader, loadKeys)
787
801
  } else {
788
- this.#loadWithFade(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate)
802
+ this.#loadWithFade(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate, partsHeader, loadKeys)
789
803
  }
790
804
  }
791
805
 
@@ -795,17 +809,20 @@ if (typeof window !== 'undefined') {
795
809
  * names, enabling per-element morphing animations orchestrated by the browser.
796
810
  * Non-transition elements still update their content within the transition frame.
797
811
  */
798
- #loadWithViewTransition(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate) {
812
+ #loadWithViewTransition(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate, partsHeader, loadKeys) {
799
813
  const oldTransitionElements = this.#applyTransitionNames()
800
814
 
815
+ const headers = {
816
+ 'X-Odac': 'ajaxload',
817
+ 'X-Odac-Load': loadKeys.join(','),
818
+ 'X-Odac-Skeleton': currentSkeleton || ''
819
+ }
820
+ if (partsHeader) headers['X-Odac-Parts'] = partsHeader
821
+
801
822
  this.#ajax({
802
823
  url,
803
824
  type: 'GET',
804
- headers: {
805
- 'X-Odac': 'ajaxload',
806
- 'X-Odac-Load': Object.keys(this.#loader.elements).join(','),
807
- 'X-Odac-Skeleton': currentSkeleton || ''
808
- },
825
+ headers,
809
826
  dataType: 'json',
810
827
  success: (data, status, xhr) => {
811
828
  const finalUrl = xhr.responseURL || url
@@ -815,6 +832,15 @@ if (typeof window !== 'undefined') {
815
832
  return
816
833
  }
817
834
 
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
+
818
844
  const transition = document.startViewTransition(() => {
819
845
  if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
820
846
 
@@ -827,10 +853,16 @@ if (typeof window !== 'undefined') {
827
853
  if (data.data) this.#data = data.data
828
854
  if (data.title) document.title = this.#decodeHtmlEntities(data.title)
829
855
 
856
+ // Update only parts that have new content from server
830
857
  elementsToUpdate.forEach(({key, element}) => {
831
858
  if (data.output && data.output[key] !== undefined) element.innerHTML = data.output[key]
832
859
  })
833
860
 
861
+ // Clear removed parts
862
+ for (const {element} of removedParts) {
863
+ element.innerHTML = ''
864
+ }
865
+
834
866
  this.#applyTransitionNames()
835
867
  })
836
868
 
@@ -854,78 +886,74 @@ if (typeof window !== 'undefined') {
854
886
 
855
887
  /**
856
888
  * Performs page navigation using the legacy fade-in/fade-out animation.
857
- * This is the fallback path when the View Transition API is unavailable
858
- * or no `odac-transition` elements exist in the DOM.
889
+ * Only fades elements whose content actually changed, leaving unchanged
890
+ * parts (like a shared sidebar) completely untouched and visible.
859
891
  */
860
- #loadWithFade(url, callback, push, currentUrl, currentSkeleton, elementsToUpdate) {
861
- let ajaxData = null,
862
- ajaxXhr = null,
863
- fadeOutComplete = false,
864
- ajaxComplete = false
865
-
866
- const applyUpdate = () => {
867
- if (!fadeOutComplete || !ajaxComplete || !ajaxData) return
868
-
869
- const finalUrl = ajaxXhr.responseURL || url
870
- if (ajaxData.skeletonChanged) {
871
- window.location.href = finalUrl
872
- return
873
- }
874
- if (finalUrl !== currentUrl && push) window.history.pushState(null, document.title, finalUrl)
875
-
876
- const newPage = ajaxXhr.getResponseHeader('X-Odac-Page')
877
- if (newPage !== null) {
878
- this.#page = newPage
879
- document.documentElement.dataset.odacPage = newPage
880
- }
881
-
882
- if (ajaxData.data) this.#data = ajaxData.data
883
- if (ajaxData.title) document.title = this.#decodeHtmlEntities(ajaxData.title)
884
-
885
- if (elementsToUpdate.length === 0) {
886
- this.#handleLoadComplete(ajaxData, callback)
887
- return
888
- }
889
-
890
- let completed = 0
891
- elementsToUpdate.forEach(({key, element}) => {
892
- if (ajaxData.output && ajaxData.output[key] !== undefined) element.innerHTML = ajaxData.output[key]
893
- this.#fadeIn(element, 200, () => {
894
- completed++
895
- if (completed === elementsToUpdate.length) this.#handleLoadComplete(ajaxData, callback)
896
- })
897
- })
898
- }
899
-
900
- if (elementsToUpdate.length > 0) {
901
- let fadeOutCount = 0
902
- elementsToUpdate.forEach(({element}) => {
903
- this.#fadeOut(element, 200, () => {
904
- fadeOutCount++
905
- if (fadeOutCount === elementsToUpdate.length) {
906
- fadeOutComplete = true
907
- applyUpdate()
908
- }
909
- })
910
- })
911
- } else {
912
- fadeOutComplete = true
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 || ''
913
897
  }
898
+ if (partsHeader) headers['X-Odac-Parts'] = partsHeader
914
899
 
915
900
  this.#ajax({
916
901
  url,
917
902
  type: 'GET',
918
- headers: {
919
- 'X-Odac': 'ajaxload',
920
- 'X-Odac-Load': Object.keys(this.#loader.elements).join(','),
921
- 'X-Odac-Skeleton': currentSkeleton || ''
922
- },
903
+ headers,
923
904
  dataType: 'json',
924
905
  success: (data, status, xhr) => {
925
- ajaxData = data
926
- ajaxXhr = xhr
927
- ajaxComplete = true
928
- applyUpdate()
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
+ })
929
957
  },
930
958
  error: () => {
931
959
  this.#isNavigating = false
@@ -974,9 +1002,10 @@ if (typeof window !== 'undefined') {
974
1002
  }
975
1003
  }
976
1004
 
977
- loader(selector, elements, callback) {
1005
+ loader(selector, elements, callback, initialParts) {
978
1006
  this.#loader.elements = elements
979
1007
  this.#loader.callback = callback
1008
+ if (initialParts) this.#loader.parts = initialParts
980
1009
  const odacInstance = this
981
1010
 
982
1011
  this.#on(document, 'click', selector, function (e) {
@@ -1152,10 +1181,28 @@ if (typeof window !== 'undefined') {
1152
1181
  window.Odac = new _odac()
1153
1182
  ;(function initAutoNavigate() {
1154
1183
  const init = () => {
1155
- const contentEl = document.querySelector('[data-odac-navigate="content"]')
1156
- if (contentEl) {
1157
- window.Odac.loader('a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)', {content: '[data-odac-navigate="content"]'}, null)
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
+ }
1158
1203
  }
1204
+
1205
+ window.Odac.loader('a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)', elements, null, initialParts)
1159
1206
  }
1160
1207
  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init()
1161
1208
  })()
@@ -1,45 +1,107 @@
1
1
  ---
2
2
  name: backend-views-templates-skill
3
- description: ODAC server-side rendering guidelines for template performance, skeleton layouts, and safe output rendering.
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, and server-side JavaScript execution via `<script:odac>`.
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. **Data Binding Two Equivalent Syntaxes**:
17
+ 2. **Part Setting**: Use `Odac.View.set(partName, viewPath)` to fill skeleton placeholders.
18
+ 3. **Data Binding — Two Equivalent Syntaxes**:
18
19
  - `<odac var="key" />`: Tag-based output (HTML-escaped, XSS-safe).
19
20
  - `{{ key }}`: Inline/interpolation output (HTML-escaped, XSS-safe). Identical behavior to `<odac var>`.
20
- - `<odac var="key" raw />` or `{!! key !!}`: Raw output (Use with extreme caution).
21
- 3. **Choosing the Right Syntax**:
22
- - **Inside HTML attributes** (`src`, `alt`, `href`, `class`, `value`, etc.) → Always prefer `{{ }}`. It reads naturally and keeps markup clean.
21
+ - `<odac get="key" />`: Query Parameter output (HTML-escaped, XSS-safe).
22
+ - `<odac var="key" raw />`, `<odac get="key" raw />` or `{!! key !!}`: Raw output (use with extreme caution).
23
+ 4. **Choosing the Right Syntax**:
24
+ - **Inside HTML attributes** (`src`, `alt`, `href`, `class`, `value`, etc.) → Always prefer `{{ }}`.
23
25
  - **Inline within text or mixed HTML** → Prefer `{{ }}` for short interpolations.
24
- - **Standalone block output** (the variable is the only content of an element) → Prefer `<odac var="" />` for structural clarity and IDE support.
25
- - Both syntaxes compile to the same engine output. The choice is about readability, not functionality.
26
- 4. **Conditionals**: Use `<odac:if condition="VAR"> ... </odac:if>`.
27
- 5. **Looping**: Use `<odac:for in="ARRAY" value="ITEM"> ... </odac:for>` or the performance-optimized `[[odac_for ...]]`.
28
- 6. **Server-Side JS**: Use `<script:odac>` for complex calculations during rendering.
26
+ - **Standalone block output** → Prefer `<odac var="" />` for structural clarity.
27
+ 5. **Conditionals**: Use `<odac:if condition="VAR"> ... </odac:if>`.
28
+ 6. **Looping**: Use `<odac:for in="ARRAY" value="ITEM"> ... </odac:for>`.
29
+ 7. **Server-Side JS**: Use `<script:odac>` for complex calculations during rendering.
30
+
31
+ ## Smart Part Diffing (AJAX Navigation)
32
+
33
+ ODAC uses a **server-driven part diffing** system during AJAX navigation. Understanding this is critical for building multi-section layouts correctly.
34
+
35
+ ### How it works
36
+ - The client tracks which view file each part is currently showing.
37
+ - On every AJAX navigation, it sends this state to the server via `X-Odac-Parts`.
38
+ - The server compares the new page's parts against the client's current state and **only renders parts that changed**.
39
+ - The client only updates DOM elements that received new content.
40
+
41
+ ### Rules
42
+ | Scenario | Behavior |
43
+ |----------|----------|
44
+ | Part view path unchanged | Skipped — not re-rendered, DOM untouched |
45
+ | Part view path changed | Re-rendered and DOM updated |
46
+ | Part removed on new page | DOM element content cleared |
47
+ | Skeleton changed | Full page reload |
48
+ | `content` part | **Always** re-rendered (URL-dependent by nature) |
49
+
50
+ ### Force-refresh a part
51
+ 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):
52
+
53
+ ```javascript
54
+ // Always re-renders on AJAX navigation even if view path is unchanged
55
+ Odac.View.set('sidebar', 'docs.nav', { refresh: true })
56
+ ```
57
+
58
+ ### Skeleton placeholder rules
59
+ - Each `{{ PLACEHOLDER }}` must be wrapped in its own HTML element.
60
+ - The engine auto-injects `data-odac-navigate` on wrapper elements — do not add manually.
61
+ - Unset placeholders are silently removed from the final HTML output.
62
+
63
+ ```html
64
+ <!-- skeleton/main.html -->
65
+ <aside>{{ SIDEBAR }}</aside> <!-- auto-gets data-odac-navigate="sidebar" -->
66
+ <main>{{ CONTENT }}</main> <!-- auto-gets data-odac-navigate="content" -->
67
+ ```
29
68
 
30
69
  ## Reference Patterns
31
70
 
32
- ### 1. The Controller to View Flow
71
+ ### 1. Standard Page with Shared Layout
72
+ ```javascript
73
+ // controller/docs/page.js
74
+ module.exports = function (Odac) {
75
+ Odac.View
76
+ .skeleton('main')
77
+ .set('sidebar', 'docs.nav') // shared — skipped if unchanged on next navigate
78
+ .set('content', 'docs.intro') // always re-rendered
79
+ }
80
+ ```
81
+
82
+ ### 2. Sidebar with Active State (force refresh)
83
+ ```javascript
84
+ // controller/docs/page.js
85
+ module.exports = function (Odac) {
86
+ Odac.View
87
+ .skeleton('main')
88
+ .set('sidebar', 'docs.nav', { refresh: true }) // re-renders every navigate
89
+ .set('content', 'docs.intro')
90
+ }
91
+ ```
92
+
93
+ ### 3. Page Without Sidebar
33
94
  ```javascript
34
- // Controller
35
- Odac.View.skeleton('main');
36
- Odac.View.set({
37
- title: 'Dashboard',
38
- stats: { users: 150, orders: 45 }
39
- });
95
+ // controller/home.js — navigating here clears the sidebar DOM element
96
+ module.exports = function (Odac) {
97
+ Odac.View
98
+ .skeleton('main')
99
+ .set('content', 'home')
100
+ // sidebar not set → its DOM element is emptied on AJAX navigation
101
+ }
40
102
  ```
41
103
 
42
- ### 2. Template Syntax Reference
104
+ ### 4. Template Syntax Reference
43
105
  ```html
44
106
  <!-- Standalone block output — prefer <odac var> -->
45
107
  <h1><odac var="title" /></h1>
@@ -47,47 +109,37 @@ Odac.View.set({
47
109
  <!-- Inside attributes — prefer {{ }} -->
48
110
  <img src="{{ product.image }}" alt="{{ product.name }}">
49
111
  <a href="/user/{{ user.id }}" class="btn {{ isActive ? 'active' : '' }}">Profile</a>
50
- <input type="text" value="{{ query }}">
51
112
 
52
- <!-- Inline text interpolation — prefer {{ }} -->
113
+ <!-- Inline text interpolation -->
53
114
  <p>Welcome, {{ user.name }}. You have {{ notifications }} new messages.</p>
54
- <span>${{ product.price }}</span>
115
+
116
+ <!-- Query parameter from URL (/page?q=search) -->
117
+ <p>Search Results for: <odac get="q" /></p>
118
+ <div class="raw-content"><odac get="htmlContent" raw /></div>
55
119
 
56
120
  <!-- Conditional -->
57
121
  <odac:if condition="stats.users > 100">
58
122
  <span class="badge">Popular!</span>
59
123
  </odac:if>
60
124
 
61
- <!-- Performance Loop -->
62
- [[odac_for user in users]]
125
+ <!-- Loop -->
126
+ <odac:for in="users" value="user">
63
127
  <li>{{ user.name }}</li>
64
- [[odac_endfor]]
128
+ </odac:for>
65
129
  ```
66
130
 
67
- ### 3. Backend JavaScript (`<script:odac>`)
68
- Perfect for calculations that shouldn't clutter the controller but are too complex for simple tags.
131
+ ### 5. Backend JavaScript (`<script:odac>`)
69
132
  ```html
70
133
  <script:odac>
71
- // Runs on the SERVER during rendering
72
- const total = items.reduce((sum, i) => sum + i.price, 0);
73
- const tax = total * 0.18;
134
+ const total = items.reduce((sum, i) => sum + i.price, 0)
135
+ const tax = total * 0.18
74
136
  </script:odac>
75
137
 
76
138
  <p>Subtotal: ${{ total }}</p>
77
139
  <p>Tax: ${{ tax }}</p>
78
140
  ```
79
141
 
80
- ## Syntax Selection Guide
81
-
82
- | Context | Preferred Syntax | Example |
83
- |---------|-----------------|---------|
84
- | HTML attributes (`src`, `href`, `alt`, `class`, `value`) | `{{ }}` | `<img src="{{ photo.url }}" alt="{{ photo.caption }}">` |
85
- | Inline text within elements | `{{ }}` | `<p>Hello, {{ user.name }}</p>` |
86
- | Standalone element content | `<odac var />` | `<h1><odac var="title" /></h1>` |
87
- | Raw HTML output (trusted only) | `<odac var raw />` or `{!! !!}` | `<div><odac var="content" raw /></div>` |
88
-
89
142
  ## Security Best Practices
90
- - **Both `{{ }}` and `<odac var>` are XSS-safe**: Both apply HTML escaping by default. Use either with confidence.
143
+ - **Both `{{ }}` and `<odac var>` are XSS-safe**: Both apply HTML escaping by default.
91
144
  - **Raw output requires trust**: Only use `raw` / `{!! !!}` with content you fully control. Never with user input.
92
145
  - **Limit `<script:odac>`**: Do not perform database queries or API calls inside views; keep them in the controller.
93
- - **Partial Awareness**: Use `<odac:include view="path.to.view" />` for reusable components.
@@ -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