metaowl 0.4.1 → 0.6.0

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +267 -2
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +144 -0
  10. package/build/runtime/modules/app-mounter.js +73 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/constants.js +38 -0
  15. package/build/runtime/modules/error-boundary.js +116 -0
  16. package/build/runtime/modules/fetch.js +31 -0
  17. package/build/runtime/modules/file-router.js +207 -0
  18. package/build/runtime/modules/fonts.js +172 -0
  19. package/build/runtime/modules/forms.js +193 -0
  20. package/build/runtime/modules/i18n.js +180 -0
  21. package/build/runtime/modules/image.js +175 -0
  22. package/build/runtime/modules/layouts.js +214 -0
  23. package/build/runtime/modules/link.js +141 -0
  24. package/build/runtime/modules/meta.js +117 -0
  25. package/build/runtime/modules/odoo-rpc.js +265 -0
  26. package/build/runtime/modules/pwa.js +272 -0
  27. package/build/runtime/modules/router.js +384 -0
  28. package/build/runtime/modules/seo.js +186 -0
  29. package/build/runtime/modules/store.js +198 -0
  30. package/build/runtime/modules/templates-manager.js +52 -0
  31. package/build/runtime/modules/test-utils.js +238 -0
  32. package/build/runtime/vite/plugin.js +197 -0
  33. package/eslint.js +29 -0
  34. package/package.json +45 -27
  35. package/CONTRIBUTING.md +0 -49
  36. package/bin/metaowl-build.js +0 -12
  37. package/bin/metaowl-dev.js +0 -12
  38. package/bin/metaowl-generate.js +0 -339
  39. package/bin/metaowl-lint.js +0 -71
  40. package/bin/utils.js +0 -82
  41. package/eslint.config.js +0 -3
  42. package/index.js +0 -328
  43. package/modules/app-mounter.js +0 -104
  44. package/modules/auto-import.js +0 -225
  45. package/modules/cache.js +0 -59
  46. package/modules/composables.js +0 -600
  47. package/modules/error-boundary.js +0 -228
  48. package/modules/fetch.js +0 -51
  49. package/modules/file-router.js +0 -478
  50. package/modules/forms.js +0 -353
  51. package/modules/i18n.js +0 -333
  52. package/modules/layouts.js +0 -431
  53. package/modules/link.js +0 -255
  54. package/modules/meta.js +0 -119
  55. package/modules/odoo-rpc.js +0 -511
  56. package/modules/pwa.js +0 -515
  57. package/modules/router.js +0 -769
  58. package/modules/seo.js +0 -501
  59. package/modules/store.js +0 -409
  60. package/modules/templates-manager.js +0 -89
  61. package/modules/test-utils.js +0 -532
  62. package/test/auto-import.test.js +0 -110
  63. package/test/cache.test.js +0 -55
  64. package/test/composables.test.js +0 -103
  65. package/test/dynamic-routes.test.js +0 -469
  66. package/test/error-boundary.test.js +0 -126
  67. package/test/fetch.test.js +0 -100
  68. package/test/file-router.test.js +0 -55
  69. package/test/forms.test.js +0 -203
  70. package/test/i18n.test.js +0 -188
  71. package/test/layouts.test.js +0 -395
  72. package/test/link.test.js +0 -189
  73. package/test/meta.test.js +0 -146
  74. package/test/odoo-rpc.test.js +0 -547
  75. package/test/pwa.test.js +0 -154
  76. package/test/router-guards.test.js +0 -229
  77. package/test/router.test.js +0 -77
  78. package/test/seo.test.js +0 -353
  79. package/test/store.test.js +0 -476
  80. package/test/templates-manager.test.js +0 -83
  81. package/test/test-utils.test.js +0 -314
  82. package/vite/plugin.js +0 -290
  83. package/vitest.config.js +0 -8
@@ -1,431 +0,0 @@
1
- /**
2
- * @module Layouts
3
- *
4
- * Layout system for OWL applications, enabling shared page structures.
5
- *
6
- * Features:
7
- * - Named layouts with automatic resolution
8
- * - Fallback to default layout
9
- * - Layout-specific state and lifecycle
10
- * - Nested layouts support
11
- * - Layout transitions
12
- *
13
- * Directory Convention:
14
- * src/
15
- * layouts/
16
- * default/
17
- * DefaultLayout.js # Default layout for all pages
18
- * DefaultLayout.xml
19
- * DefaultLayout.css
20
- * auth/
21
- * AuthLayout.js # Layout for auth pages (login, register)
22
- * admin/
23
- * AdminLayout.js # Layout with sidebar for admin pages
24
- *
25
- * Usage in Pages:
26
- * export class MyPage extends Component {
27
- * static layout = 'admin' // Use admin layout
28
- * }
29
- *
30
- * Layout Template Convention:
31
- * <templates>
32
- * <t t-name="DefaultLayout">
33
- * <div class="layout-default">
34
- * <header t-name="header"/>
35
- * <main>
36
- * <t t-slot="default"/> <!-- Page content renders here -->
37
- * </main>
38
- * <footer t-name="footer"/>
39
- * </div>
40
- * </t>
41
- * </templates>
42
- *
43
- * @example
44
- * // layouts/default/DefaultLayout.js
45
- * import { Component } from '@odoo/owl'
46
- *
47
- * export class DefaultLayout extends Component {
48
- * static template = 'DefaultLayout'
49
- * }
50
- *
51
- * // pages/index/Index.js
52
- * import { Component } from '@odoo/owl'
53
- *
54
- * export class IndexPage extends Component {
55
- * static template = 'IndexPage'
56
- * static layout = 'default' // Optional, 'default' is implicit
57
- * }
58
- */
59
-
60
- import { Component, xml, mount } from '@odoo/owl'
61
-
62
- /**
63
- * Registry of layout components.
64
- * @type {Map<string, typeof Component>}
65
- */
66
- const _layouts = new Map()
67
-
68
- /**
69
- * Default layout name used when none specified.
70
- * @type {string}
71
- */
72
- let _defaultLayout = 'default'
73
-
74
- /**
75
- * Current active layout instance.
76
- * @type {Component|null}
77
- */
78
- let _currentLayout = null
79
-
80
- /**
81
- * Layout change listeners.
82
- * @type {Function[]}
83
- */
84
- const _listeners = []
85
-
86
- /**
87
- * Layout configuration per route.
88
- * @type {Map<string, string>}
89
- */
90
- const _routeLayouts = new Map()
91
-
92
- /**
93
- * Register a layout component.
94
- *
95
- * @param {string} name - Layout identifier
96
- * @param {typeof Component} layoutComponent - OWL component class
97
- * @param {object} options - Layout options
98
- * @param {boolean} [options.default=false] - Set as default layout
99
- *
100
- * @example
101
- * import { DefaultLayout } from './layouts/default/DefaultLayout.js'
102
- * import { AdminLayout } from './layouts/admin/AdminLayout.js'
103
- *
104
- * registerLayout('default', DefaultLayout, { default: true })
105
- * registerLayout('admin', AdminLayout)
106
- */
107
- export function registerLayout(name, layoutComponent, options = {}) {
108
- _layouts.set(name, layoutComponent)
109
-
110
- if (options.default) {
111
- _defaultLayout = name
112
- }
113
-
114
- // Notify listeners
115
- for (const listener of _listeners) {
116
- listener({ type: 'register', name, layout: layoutComponent })
117
- }
118
- }
119
-
120
- /**
121
- * Unregister a layout.
122
- *
123
- * @param {string} name - Layout identifier
124
- * @returns {boolean} True if layout was removed
125
- */
126
- export function unregisterLayout(name) {
127
- const removed = _layouts.delete(name)
128
-
129
- if (removed) {
130
- for (const listener of _listeners) {
131
- listener({ type: 'unregister', name })
132
- }
133
- }
134
-
135
- return removed
136
- }
137
-
138
- /**
139
- * Get a registered layout by name.
140
- *
141
- * @param {string} name - Layout identifier
142
- * @returns {typeof Component|undefined}
143
- */
144
- export function getLayout(name) {
145
- return _layouts.get(name)
146
- }
147
-
148
- /**
149
- * Check if a layout is registered.
150
- *
151
- * @param {string} name - Layout identifier
152
- * @returns {boolean}
153
- */
154
- export function hasLayout(name) {
155
- return _layouts.has(name)
156
- }
157
-
158
- /**
159
- * Get all registered layout names.
160
- *
161
- * @returns {string[]}
162
- */
163
- export function getLayoutNames() {
164
- return Array.from(_layouts.keys())
165
- }
166
-
167
- /**
168
- * Set the default layout name.
169
- *
170
- * @param {string} name - Layout identifier
171
- */
172
- export function setDefaultLayout(name) {
173
- if (!_layouts.has(name)) {
174
- console.warn(`[metaowl] Layout "${name}" is not registered yet`)
175
- }
176
- _defaultLayout = name
177
- }
178
-
179
- /**
180
- * Get the current default layout name.
181
- *
182
- * @returns {string}
183
- */
184
- export function getDefaultLayout() {
185
- return _defaultLayout
186
- }
187
-
188
- /**
189
- * Resolve layout name for a given component or route.
190
- *
191
- * @param {typeof Component} component - Page component
192
- * @param {string} [routePath] - Optional route path for route-specific layout
193
- * @returns {string} Layout name
194
- */
195
- export function resolveLayout(component, routePath) {
196
- // Check route-specific layout first
197
- if (routePath && _routeLayouts.has(routePath)) {
198
- return _routeLayouts.get(routePath)
199
- }
200
-
201
- // Check component static property
202
- if (component.layout) {
203
- return component.layout
204
- }
205
-
206
- // Check for layout defined via decorator/metadata
207
- if (component._layout) {
208
- return component._layout
209
- }
210
-
211
- // Fall back to default
212
- return _defaultLayout
213
- }
214
-
215
- /**
216
- * Assign a layout to a specific route.
217
- *
218
- * @param {string} routePath - Route path pattern
219
- * @param {string} layoutName - Layout identifier
220
- */
221
- export function setRouteLayout(routePath, layoutName) {
222
- _routeLayouts.set(routePath, layoutName)
223
- }
224
-
225
- /**
226
- * Get layout assigned to a route.
227
- *
228
- * @param {string} routePath - Route path
229
- * @returns {string|undefined}
230
- */
231
- export function getRouteLayout(routePath) {
232
- return _routeLayouts.get(routePath)
233
- }
234
-
235
- /**
236
- * Create a layout wrapper component that renders the page inside the layout.
237
- *
238
- * @param {typeof Component} layoutComponent - Layout component class
239
- * @param {typeof Component} pageComponent - Page component class
240
- * @param {object} [props] - Props to pass to page component
241
- * @returns {typeof Component} Wrapper component
242
- */
243
- export function createLayoutWrapper(layoutComponent, pageComponent, props = {}) {
244
- const LayoutClass = layoutComponent
245
- const PageClass = pageComponent
246
-
247
- return class LayoutWrapper extends Component {
248
- static template = xml`
249
- <t t-component="layout" t-props="layoutProps">
250
- <t t-component="page" t-props="pageProps"/>
251
- </t>
252
- `
253
-
254
- setup() {
255
- this.layout = LayoutClass
256
- this.page = PageClass
257
- this.layoutProps = {}
258
- this.pageProps = props
259
- }
260
- }
261
- }
262
-
263
- /**
264
- * Mount a page component within its resolved layout.
265
- *
266
- * @param {typeof Component} pageComponent - Page component to mount
267
- * @param {HTMLElement} target - Mount target element
268
- * @param {object} [options] - Mount options
269
- * @param {string} [options.routePath] - Current route path
270
- * @param {object} [options.props] - Props for page component
271
- * @param {object} [config] - OWL mount configuration
272
- * @returns {Promise<Component>} Mounted component instance
273
- */
274
- export async function mountWithLayout(pageComponent, target, options = {}, config = {}) {
275
- const { routePath, props = {}, templates } = options
276
-
277
- const layoutName = resolveLayout(pageComponent, routePath)
278
- const LayoutClass = getLayout(layoutName)
279
-
280
- if (!LayoutClass) {
281
- console.warn(`[metaowl] Layout "${layoutName}" not found, mounting page without layout`)
282
- return mount(pageComponent, target, { ...config, props, templates })
283
- }
284
-
285
- // Create wrapper that combines layout and page
286
- const WrapperClass = createLayoutWrapper(LayoutClass, pageComponent, props)
287
-
288
- const instance = await mount(WrapperClass, target, { ...config, templates })
289
-
290
- _currentLayout = instance
291
-
292
- // Notify listeners
293
- for (const listener of _listeners) {
294
- listener({ type: 'mount', layout: layoutName, page: pageComponent.name })
295
- }
296
-
297
- return instance
298
- }
299
-
300
- /**
301
- * Get the currently active layout instance.
302
- *
303
- * @returns {Component|null}
304
- */
305
- export function getCurrentLayout() {
306
- return _currentLayout
307
- }
308
-
309
- /**
310
- * Subscribe to layout events.
311
- *
312
- * @param {Function} callback - Event listener
313
- * @returns {Function} Unsubscribe function
314
- */
315
- export function subscribeToLayouts(callback) {
316
- _listeners.push(callback)
317
- return () => {
318
- const index = _listeners.indexOf(callback)
319
- if (index > -1) _listeners.splice(index, 1)
320
- }
321
- }
322
-
323
- /**
324
- * Clear all layouts and reset state (useful for testing).
325
- */
326
- export function clearLayouts() {
327
- _layouts.clear()
328
- _routeLayouts.clear()
329
- _listeners.length = 0
330
- _defaultLayout = 'default'
331
- _currentLayout = null
332
- }
333
-
334
- /**
335
- * Layout decorator for setting layout on component class.
336
- *
337
- * @param {string} name - Layout identifier
338
- * @returns {Function} Decorator function
339
- *
340
- * @example
341
- * @layout('admin')
342
- * export class AdminPage extends Component {
343
- * // ...
344
- * }
345
- */
346
- export function layout(name) {
347
- return function decorator(ComponentClass) {
348
- ComponentClass.layout = name
349
- return ComponentClass
350
- }
351
- }
352
-
353
- /**
354
- * DefineLayout decorator with additional options.
355
- *
356
- * @param {string} name - Layout identifier
357
- * @param {object} options - Layout options
358
- * @returns {Function} Decorator function
359
- *
360
- * @example
361
- * @defineLayout('admin', { persistent: true })
362
- * export class AdminPage extends Component {
363
- * // ...
364
- * }
365
- */
366
- export function defineLayout(name, options = {}) {
367
- return function decorator(ComponentClass) {
368
- ComponentClass.layout = name
369
- ComponentClass.layoutOptions = options
370
- return ComponentClass
371
- }
372
- }
373
-
374
- /**
375
- * Build layouts from import.meta.glob result (file-based layouts).
376
- *
377
- * Convention:
378
- * './layouts/default/DefaultLayout.js' → layout 'default'
379
- * './layouts/admin/AdminLayout.js' → layout 'admin'
380
- *
381
- * @param {Record<string, object>} modules - import.meta.glob result
382
- * @returns {object} Map of layout names to component classes
383
- */
384
- export function buildLayouts(modules) {
385
- const layouts = {}
386
-
387
- for (const [key, mod] of Object.entries(modules)) {
388
- // Extract layout name from path: './layouts/default/DefaultLayout.js'
389
- const match = key.match(/\.\/layouts\/([^/]+)/)
390
- if (!match) continue
391
-
392
- const layoutName = match[1]
393
- const ComponentClass = mod.default || Object.values(mod).find(v => typeof v === 'function')
394
-
395
- if (ComponentClass) {
396
- layouts[layoutName] = ComponentClass
397
- registerLayout(layoutName, ComponentClass)
398
- }
399
- }
400
-
401
- return layouts
402
- }
403
-
404
- /**
405
- * Auto-discover and register layouts from the layouts directory.
406
- *
407
- * @param {object} options
408
- * @param {string} [options.defaultLayout='default'] - Default layout name
409
- * @returns {Promise<object>} Registered layouts
410
- *
411
- * @example
412
- * // metaowl.js
413
- * import { discoverLayouts } from 'metaowl'
414
- *
415
- * await discoverLayouts({ defaultLayout: 'default' })
416
- * boot()
417
- */
418
- export async function discoverLayouts(options = {}) {
419
- const { defaultLayout = 'default' } = options
420
-
421
- // This will be transformed by Vite plugin at build time
422
- const modules = import.meta.glob('./layouts/**/*.js', { eager: true })
423
- const layouts = buildLayouts(modules)
424
-
425
- // Set default if specified layout exists
426
- if (layouts[defaultLayout]) {
427
- setDefaultLayout(defaultLayout)
428
- }
429
-
430
- return layouts
431
- }
package/modules/link.js DELETED
@@ -1,255 +0,0 @@
1
- /**
2
- * @module Link
3
- *
4
- * SPA Link component for metaowl with automatic external link detection.
5
- *
6
- * This component renders a link that navigates via history.pushState
7
- * for internal targets (without page reload) and navigates normally
8
- * for external targets.
9
- *
10
- * Features:
11
- * - Automatic detection of external links (http://, https://, //, mailto:, tel:, etc.)
12
- * - SPA navigation for internal links (no page reload)
13
- * - Support for active link styling
14
- * - Respects modifier keys (Ctrl, Meta, Alt) for normal browser navigation
15
- * - Accessible links with correct href attributes
16
- *
17
- * @example
18
- * // Internal link
19
- * <t-link to="/about">About Us</t-link>
20
- *
21
- * // With CSS classes
22
- * <t-link to="/user/profile" class="btn btn-primary">Profile</t-link>
23
- *
24
- * // Active link styling
25
- * <t-link to="/about" activeClass="active">About Us</t-link>
26
- *
27
- * // External link (automatically detected)
28
- * <t-link to="https://example.com">External</t-link>
29
- */
30
-
31
- import { Component, useState, onMounted, onWillUnmount } from '@odoo/owl'
32
-
33
- /**
34
- * Regex for detecting external URLs.
35
- * Matches: http://, https://, // (protocol-relative), mailto:, tel:, ftp:, etc.
36
- * @type {RegExp}
37
- */
38
- const EXTERNAL_URL_REGEX = /^(https?:|\/\/|mailto:|tel:|ftp:|file:|javascript:)/i
39
-
40
- /**
41
- * Checks if a URL is external.
42
- *
43
- * @param {string} url - The URL to check
44
- * @returns {boolean} True if external
45
- */
46
- function isExternalUrl(url) {
47
- if (!url || typeof url !== 'string') return false
48
- return EXTERNAL_URL_REGEX.test(url)
49
- }
50
-
51
- /**
52
- * Checks if a link is considered "active" (for styling).
53
- *
54
- * @param {string} linkPath - The link path
55
- * @param {string} currentPath - The current path
56
- * @returns {boolean} True if active
57
- */
58
- function isActiveLink(linkPath, currentPath) {
59
- if (!linkPath || !currentPath) return false
60
- // Exact match or subpath
61
- const normalizedLink = linkPath.replace(/\/$/, '') || '/'
62
- const normalizedCurrent = currentPath.replace(/\/$/, '') || '/'
63
- return normalizedCurrent === normalizedLink ||
64
- (normalizedLink !== '/' && normalizedCurrent.startsWith(normalizedLink + '/'))
65
- }
66
-
67
- /**
68
- * Link component for SPA navigation.
69
- *
70
- * Renders an <a> element that performs internal navigation
71
- * without page reload.
72
- */
73
- export class Link extends Component {
74
- static template = 'Link'
75
- static props = {
76
- to: { type: String, optional: false },
77
- class: { type: String, optional: true },
78
- activeClass: { type: String, optional: true },
79
- target: { type: String, optional: true },
80
- rel: { type: String, optional: true },
81
- title: { type: String, optional: true },
82
- download: { type: [String, Boolean], optional: true },
83
- hreflang: { type: String, optional: true },
84
- type: { type: String, optional: true },
85
- ping: { type: String, optional: true },
86
- referrerpolicy: { type: String, optional: true },
87
- media: { type: String, optional: true },
88
- '*': true,
89
- }
90
-
91
- setup() {
92
- this.state = useState({
93
- isActive: false
94
- })
95
-
96
- // Reference to navigation function (injected from outside)
97
- this._navigate = null
98
-
99
- onMounted(() => {
100
- this._updateActiveState()
101
- // Listen to PopState events for updating active status
102
- window.addEventListener('popstate', this._updateActiveState)
103
- })
104
-
105
- onWillUnmount(() => {
106
- window.removeEventListener('popstate', this._updateActiveState)
107
- })
108
-
109
- this._updateActiveState = () => {
110
- if (this.props.activeClass) {
111
- this.state.isActive = isActiveLink(this.props.to, document.location.pathname)
112
- }
113
- }
114
- }
115
-
116
- /**
117
- * Getter for combined CSS classes.
118
- * @returns {string}
119
- */
120
- get linkClasses() {
121
- const classes = []
122
- if (this.props.class) {
123
- classes.push(this.props.class)
124
- }
125
- if (this.state.isActive && this.props.activeClass) {
126
- classes.push(this.props.activeClass)
127
- }
128
- return classes.join(' ')
129
- }
130
-
131
- /**
132
- * Getter for the rel attribute.
133
- * Automatically adds noopener noreferrer for external links with target="_blank".
134
- * @returns {string|undefined}
135
- */
136
- get linkRel() {
137
- if (this.props.rel) return this.props.rel
138
- if (isExternalUrl(this.props.to) && this.props.target === '_blank') {
139
- return 'noopener noreferrer'
140
- }
141
- return undefined
142
- }
143
-
144
- /**
145
- * Forward unknown component props as native <a> attributes.
146
- * This allows id/style/aria-* / data-* and similar anchor attributes.
147
- * @returns {Record<string, any>}
148
- */
149
- get forwardedAttrs() {
150
- const attrs = { ...this.props }
151
- delete attrs.to
152
- delete attrs.class
153
- delete attrs.activeClass
154
- delete attrs.target
155
- delete attrs.rel
156
- delete attrs.title
157
- delete attrs.download
158
- return attrs
159
- }
160
-
161
- /**
162
- * Handler for click events.
163
- * Checks if SPA navigation is possible or normal navigation should be used.
164
- *
165
- * @param {MouseEvent} ev - The click event
166
- */
167
- onClick(ev) {
168
- const url = this.props.to
169
-
170
- // External URLs: Normal navigation
171
- if (isExternalUrl(url)) {
172
- return // Let browser handle normal navigation
173
- }
174
-
175
- // Modifier keys: Normal navigation (new tab, download, etc.)
176
- if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) {
177
- return
178
- }
179
-
180
- // Right click: Context menu
181
- if (ev.button !== 0) {
182
- return
183
- }
184
-
185
- // Download link: Normal navigation
186
- if (this.props.download) {
187
- return
188
- }
189
-
190
- // Internal SPA navigation
191
- ev.preventDefault()
192
-
193
- // Update URL immediately so components reading window.location.pathname
194
- // (e.g. isActive in sidebar) already see the correct path when the new
195
- // app mounts. The generation counter in _spaNavigate guarantees that only
196
- // the last-triggered navigation actually calls mountApp.
197
- window.history.pushState({ path: url }, '', url)
198
-
199
- if (typeof window.__metaowlNavigate === 'function') {
200
- window.__metaowlNavigate(url)
201
- } else {
202
- // Fallback: normal navigation (URL already updated above)
203
- window.location.href = url
204
- }
205
- }
206
- }
207
-
208
- /**
209
- * Template for the Link component.
210
- * Must be registered in the app's templates file or loaded dynamically.
211
- */
212
- export const LinkTemplate = /* xml */ `
213
- <templates>
214
- <t t-name="Link">
215
- <a
216
- t-att="forwardedAttrs"
217
- t-att-href="props.to"
218
- t-att-class="linkClasses"
219
- t-att-target="props.target"
220
- t-att-rel="linkRel"
221
- t-att-title="props.title"
222
- t-att-download="props.download"
223
- t-on-click="onClick"
224
- >
225
- <t t-slot="default"/>
226
- </a>
227
- </t>
228
- </templates>
229
- `
230
-
231
- /**
232
- * Helper function to register the Link template.
233
- * Called automatically on app startup.
234
- *
235
- * @param {object} templates - The app's templates object
236
- * @returns {string|void} Modified templates if string was passed
237
- */
238
- export function registerLinkTemplate(templates) {
239
- if (typeof templates === 'string') {
240
- // If templates is a string, add the Link template
241
- // Remove outer <templates> tags from LinkTemplate
242
- const linkContent = LinkTemplate
243
- .replace('<templates>', '')
244
- .replace('</templates>', '')
245
- .trim()
246
- // Insert before closing </templates> tag
247
- return templates.replace('</templates>', linkContent + '\n</templates>')
248
- }
249
- // If templates is an object, register the template
250
- if (templates && typeof templates === 'object') {
251
- templates.Link = LinkTemplate
252
- }
253
- }
254
-
255
- export default Link