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.
@@ -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 eliminates the need for complex client-side code.
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. "&amp;" → "&").
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
- const elementsToUpdate = []
737
- elements.forEach(([key, selector]) => {
738
- const element = document.querySelector(selector)
739
- if (element) elementsToUpdate.push({key, element})
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
- let ajaxData = null,
743
- ajaxXhr = null,
744
- fadeOutComplete = false,
745
- ajaxComplete = false
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
- const applyUpdate = () => {
748
- if (!fadeOutComplete || !ajaxComplete || !ajaxData) return
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
- const finalUrl = ajaxXhr.responseURL || url
751
- if (ajaxData.skeletonChanged) {
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
- const newPage = ajaxXhr.getResponseHeader('X-Odac-Page')
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
- if (ajaxData.data) this.#data = ajaxData.data
764
- if (ajaxData.title) document.title = ajaxData.title
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
- if (elementsToUpdate.length === 0) {
767
- this.#handleLoadComplete(ajaxData, callback)
768
- return
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
- let completed = 0
772
- elementsToUpdate.forEach(({key, element}) => {
773
- if (ajaxData.output && ajaxData.output[key] !== undefined) element.innerHTML = ajaxData.output[key]
774
- this.#fadeIn(element, 200, () => {
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
- if (elementsToUpdate.length > 0) {
782
- let fadeOutCount = 0
783
- elementsToUpdate.forEach(({element}) => {
784
- this.#fadeOut(element, 200, () => {
785
- fadeOutCount++
786
- if (fadeOutCount === elementsToUpdate.length) {
787
- fadeOutComplete = true
788
- applyUpdate()
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
- } else {
793
- fadeOutComplete = true
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: 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
- ajaxData = data
807
- ajaxXhr = xhr
808
- ajaxComplete = true
809
- 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
+ })
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 contentEl = document.querySelector('[data-odac-navigate="content"]')
1037
- if (contentEl) {
1038
- 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
+ }
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 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**:
18
- - `{{ key }}`: Escaped output (Standard).
19
- - `{!! key !!}`: Raw output (Use with extreme caution).
20
- 3. **Conditionals**: Use `<odac:if condition="VAR"> ... </odac:if>`.
21
- 4. **Looping**: Use `<odac:for in="ARRAY" value="ITEM"> ... </odac:for>` or the performance-optimized `[[odac_for ...]]`.
22
- 5. **Server-Side JS**: Use `<script:odac>` for complex calculations during rendering.
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. The Controller to View Flow
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
- // Controller
29
- Odac.View.skeleton('main');
30
- Odac.View.set({
31
- title: 'Dashboard',
32
- stats: { users: 150, orders: 45 }
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
- ### 2. Template Syntax Reference
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
- <!-- Display Variable -->
39
- <h1>{{ title }}</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
- <!-- Performance Loop -->
47
- [[odac_for user in users]]
120
+ <!-- Loop -->
121
+ <odac:for in="users" value="user">
48
122
  <li>{{ user.name }}</li>
49
- [[odac_endfor]]
123
+ </odac:for>
50
124
  ```
51
125
 
52
- ### 3. Backend JavaScript (`<script:odac>`)
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
- // Runs on the SERVER during rendering
57
- const total = items.reduce((sum, i) => sum + i.price, 0);
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
- - **Always use `{{ }}`**: Standard tags prevent XSS.
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.