posthtml-component 2.0.0-beta.1 → 2.0.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.
package/README.md CHANGED
@@ -1,1043 +1,1062 @@
1
- [![NPM][npm]][npm-url]
2
- [![Coverage][cover]][cover-badge]
3
- [![XO code style][style]][style-url]
4
-
5
- <div align="center">
6
- <img width="300" title="PostHTML" src="http://posthtml.github.io/posthtml/logo.svg">
7
- <h1>PostHTML Components </h1>
8
- <p>A PostHTML plugin for create components with HTML-friendly syntax inspired by Laravel Blade. Slots, stack/push, props, custom tag and much more.</p>
9
- </div>
10
-
11
- ## Installation
12
-
13
- ```bash
14
- npm i -D posthtml-component
15
- ```
16
-
17
- ## Introduction
18
-
19
- This PostHTML plugin provides an HTML-friendly syntax for write components in your templates.
20
- If you are familiar with Blade, React, Vue or similar, you will find familiar syntax as this plugin is inspired by them.
21
- See below a basic example, as code is worth a thousand words.
22
-
23
- **See also the first [PostHTML Bootstrap UI](https://github.com/thewebartisan7/posthtml-bootstrap-ui) using this plugin and check also the [starter template here](https://github.com/thewebartisan7/posthtml-bootstrap-ui-starter).**
24
-
25
- ## Basic example
26
-
27
- Create the component:
28
-
29
- ``` html
30
- <!-- src/button.html -->
31
- <button type="button" class="btn">
32
- <yield></yield>
33
- </button>
34
- ```
35
-
36
- Use the component:
37
-
38
- ``` html
39
- <!-- src/index.html -->
40
- <html>
41
- <body>
42
- <x-button type="submit" class="btn-primary">Submit</x-button>
43
- </body>
44
- </html>
45
- ```
46
-
47
- Init PostHTML:
48
-
49
- ```js
50
- // index.js
51
- const { readFileSync, writeFileSync } = require('fs')
52
-
53
- const posthtml = require('posthtml')
54
- const components = require('posthtml-components')
55
-
56
- posthtml(components({ root: './src' }))
57
- .process(readFileSync('src/index.html', 'utf8'))
58
- .then((result) => writeFileSync('dist/index.html', result.html, 'utf8'))
59
- ```
60
-
61
- Result:
62
-
63
- ``` html
64
- <!-- dist/index.html -->
65
- <html>
66
- <body>
67
- <button type="submit" class="btn btn-primary">Submit</button>
68
- </body>
69
- </html>
70
- ```
71
-
72
- You may notice that the `src/button.html` component has a `type` and `class` attribute, and when we use the component in `src/index.html` we pass `type` and `class` attribute.
73
- The result is that `type` is override, and `class` is merged.
74
-
75
- By default `class` and `style` attributes are merged, while all others attribute are override.
76
- You can also override `class` and `style` attributes by prepending `override:` to the class attribute. Example:
77
-
78
- ```html
79
- <x-button override:class="btn-custom">Submit</x-button>
80
-
81
- <!-- Output -->
82
- <button type="button" class="btn-custom">Submit</button>
83
- ```
84
-
85
- All attributes you pass to the component will be added to the first node of your component or to the node with an attribute names `attributes`,
86
- and only if they are not defined as `props` via `<script props>` or if they are not in the following file
87
- [valid-attributes.js](https://github.com/thewebartisan7/posthtml-components/blob/main/src/valid-attributes.js).
88
- You can also manage valid attributes via options.
89
- More details on this in [Attributes](#attributes) section.
90
-
91
- The `<yield>` tag is where your content will be injected.
92
- In next section you can find all available options and then examples for each feature.
93
-
94
- See also the `docs-src` folder where you can find more examples.
95
- You can run `npm run build` to compile them.
96
-
97
- ## Options
98
-
99
- | Option | Default | Description |
100
- |:------------------------:|:---------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------|
101
- | **root** | `'./'` | String value as root path for components lookup. |
102
- | **folders** | `['']` | Array of additional multi folders path from `options.root` or any defined namespaces root, fallback or custom. |
103
- | **tagPrefix** | `x-` | String for tag prefix. The plugin will use RegExp with this string. |
104
- | **tag** | `false` | String or boolean value for component tag. Use this with `options.attribute`. Boolean only false. |
105
- | **attribute** | `src` | String value for component attribute for set path. |
106
- | **namespaces** | `[]` | Array of namespace's root path, fallback path and custom path for override. |
107
- | **namespaceSeparator** | `::` | String value for namespace separator to be used with tag name. Example `<x-namespace::button>` |
108
- | **fileExtension** | `html` | String value for file extension of the components used for retrieve x-tag file. |
109
- | **yield** | `yield` | String value for `<yield>` tag name. Where main content of component is injected. |
110
- | **slot** | `slot` | String value for `<slot>` tag name. Used with RegExp by appending `:` (example `<slot:slot-name>`). |
111
- | **fill** | `fill` | String value for `<fill>` tag name. Used with RegExp by appending `:` (example `<fill:slot-name>`). |
112
- | **slotSeparator** | `:` | String value used for separate `<slot>` and `<fill>` tag from their names. |
113
- | **push** | `push` | String value for `<push>` tag name. |
114
- | **stack** | `stack` | String value for `<stack>` tag name. |
115
- | **propsScriptAttribute** | `props` | String value used as attribute in `<script props>` parsed by the plugin to retrieve props of the component. |
116
- | **propsContext** | `props` | String value used as object name inside the script to process process before passed to the component. |
117
- | **propsAttribute** | `props` | String value for props attribute to define props as JSON. |
118
- | **propsSlot** | `props` | String value used to retrieve the props passed to slot via `$slots.slotName.props`. |
119
- | **parserOptions** | `{recognizeSelfClosing: true}` | Object to configure `posthtml-parser`. By default, it enables support for self-closing component tags. |
120
- | **expressions** | `{}` | Object to configure `posthtml-expressions`. You can pre-set locals or customize the delimiters for example. |
121
- | **plugins** | `[]` | PostHTML plugins to apply for every parsed components. |
122
- | **matcher** | `[{tag: options.tagPrefix}]` | Array of object used to match the tags. |
123
- | **attrsParserRules** | `{}` | Additional rules for attributes parser plugin. |
124
- | **strict** | `true` | Boolean value for enable or disable throw an exception. |
125
- | **mergeCustomizer** | `function` | Function callback passed to lodash `mergeWith` for merge `options.expressions.locals` and props passed via attribute `props`. |
126
- | **utilities** | `{merge: _.mergeWith, template: _.template}` | Object of utilities methods to be passed to `<script props>`. By default lodash `mergeWith` and `template`. |
127
- | **elementAttributes** | `{}` | An object with tag name and a function modifier of valid-attributes.js. |
128
- | **safelistAttributes** | `['data-*']` | An array of attributes name to be added to default valid attributes. |
129
- | **blacklistAttributes** | `[]` | An array of attributes name to be removed from default valid attributes. |
130
-
131
- ## Features
132
-
133
- ### Tag names and x-tags
134
-
135
- You can use the components in multiple ways, or also a combination of them.
136
- Like with `posthtml-extend` and `posthtml-modules` you can define a tag name in combination with an attribute name for set the path of the components.
137
-
138
- For example for the same button component `src/button.html` in the basic example we can define the tag name and attribute name and then use it in this way:
139
-
140
- ``` html
141
- <!-- src/index.html -->
142
- <html>
143
- <body>
144
- <component src="button.html">Submit</component>
145
- </body>
146
- </html>
147
- ```
148
-
149
- Init PostHTML:
150
-
151
- ```js
152
- // index.js
153
-
154
- require('posthtml')(require('posthtml-components')({ root: './src', tagName: 'component', attribute: 'src' }))
155
- .process(/* ... */)
156
- .then(/* ... */)
157
- ```
158
-
159
- If you need more control over how to match the tags, you can pass directly an array of matcher or single object via `options.matcher` like shown in below example:
160
-
161
- ```js
162
- // index.js
163
-
164
- const options = {
165
- root: './src',
166
- matcher: [{tag: 'a-tag'}, {tag: 'another-one'}, {tag: new RegExp(`^app-`, 'i')}]
167
- };
168
-
169
- require('posthtml')(require('posthtml-components')(options))
170
- .process(/* ... */)
171
- .then(/* ... */)
172
- ```
173
-
174
- With `posthtml-components` you don't need to specify the path name when you are using `x-tag-name` syntax. See below example.
175
-
176
- Setup PostHTML:
177
-
178
- ```js
179
- // index.js
180
-
181
- const options = {
182
- root: './src',
183
- tagPrefix: 'x-'
184
- };
185
-
186
- require('posthtml')(require('posthtml-components')(options))
187
- .process(/* ... */)
188
- .then(/* ... */)
189
- ```
190
-
191
- Use:
192
-
193
- ``` html
194
- <!-- src/index.html -->
195
- <html>
196
- <body>
197
- <x-button>Submit</x-button>
198
- </body>
199
- </html>
200
- ```
201
-
202
- If your components are in a subfolder then you can use `dot` to access it, example:
203
-
204
- ``` html
205
- <!-- Supposing your button component is located in ./src/components/forms/button.html -->
206
- <x-forms.button>Submit</x-forms.button>
207
- ```
208
-
209
- If your components are in a sub-folder with multiple files, then for avoid typing the main file you can use `index.html` without specify it.
210
- Please see below example to understand better.
211
-
212
- ``` html
213
- <!-- Supposing your modal component is located in ./src/components/modals/index.html -->
214
- <x-modal.index>Submit</x-modal.index>
215
-
216
- <!-- You can omit "index" part since the file is named "index.html" -->
217
- <x-modal>Submit</x-modal>
218
- ```
219
-
220
- #### Parser options
221
-
222
- You may pass options to `posthtml-parser` via `options.parserOptions`.
223
-
224
- ```js
225
- // index.js
226
- const options = {
227
- root: './src',
228
- parserOptions: { decodeEntities: true }
229
- };
230
-
231
- require('posthtml')(require('posthtml-components')(options))
232
- .process('some HTML', options.parserOptions)
233
- .then(/* ... */)
234
- ```
235
-
236
- Important: as you can see, whatever `parserOptions` you pass to the plugin, must also be passed in the `process` method in your code, otherwise your PostHTML build will use `posthtml-parser` defaults and will override anything you've passed to `posthtml-component`.
237
-
238
- #### Self-closing tags
239
-
240
- The plugin supports self-closing tags by default, but you need to make sure to enable them in the `process` method in your code too, by passing `recognizeSelfClosing: true` in the options object:
241
-
242
- ```js
243
- // index.js
244
- require('posthtml')(require('posthtml-components')({root: './src'}))
245
- .process('your HTML...', {recognizeSelfClosing: true})
246
- .then(/* ... */)
247
- ```
248
-
249
- If you don't add this to `process`, PostHTML will use `posthtml-parser` defaults and will not support self-closing component tags. This will result in everything after a self-closing tag not being output.
250
-
251
- ### Multiple folders
252
-
253
- You have full control where to place your components. Once you set the base root path of your components, you can then set multiple folders.
254
- For example let's suppose your main root is `./src` and then you have several folders where you have your components, for example `./src/components` and `./src/layouts`.
255
- You can set up the plugin like below:
256
-
257
- ```js
258
- // index.js
259
- const options = {
260
- root: './src',
261
- folders: ['components', 'layouts']
262
- };
263
-
264
- require('posthtml')(require('posthtml-components')(options))
265
- .process(/* ... */)
266
- .then(/* ... */)
267
- ```
268
-
269
- ### Namespaces
270
-
271
- With namespaces, you can define a top level root path to your components like shown in below example.
272
- It can be useful for handle custom theme, where you define a specific top level root, with fallback root when component it's not found,
273
- and a custom root for override, something like a child theme.
274
-
275
- Thanks to namespace, you can create folders structure like below:
276
-
277
- - `src` (base root folder)
278
- - `components` (folder for components like modal, button, etc.)
279
- - `layouts` (folder for layout components like base layout, header, footer, etc.)
280
- - `theme-dark` (namespace folder for theme-dark)
281
- - `components` (folder for components for theme dark)
282
- - `layouts` (folder for layout components for dark theme)
283
- - `theme-light` (namespace folder for theme-light)
284
- - `components` (folder for components for light theme)
285
- - `layouts` (folder for layout components for dark theme)
286
- - `custom` (custom folder for override your namespace themes)
287
- - `theme-dark` (custom folder for override dark theme)
288
- - `components` (folder for override components of theme dark)
289
- - `layouts` (folder for override layout components of dark theme)
290
- - `theme-light` (custom folder for override light theme)
291
- - `components` (folder for override components of theme dark)
292
- - `layouts` (folder for override layout components of dark theme)
293
-
294
- And the options would be like:
295
-
296
- ```js
297
- // index.js
298
- const options = {
299
- // Main root for component without namespace
300
- root: './src',
301
- // Folders is always appended in 'root' or any defined namespace's folders (base, fallback or custom)
302
- folders: ['components', 'layouts'],
303
- namespaces: [{
304
- // Namespace name will be prepend to tag name (example <x-theme-dark::button>)
305
- name: 'theme-dark',
306
- // Base root of the namespace
307
- root: './src/theme-dark',
308
- // Fallback root when a component it's not found in namespace
309
- fallback: './src',
310
- // Custom root for override, the lookup happen first here
311
- custom: './src/custom/theme-dark'
312
- }, {
313
- // Light theme
314
- name: 'theme-light',
315
- root: './src/theme-light',
316
- fallback: './src',
317
- custom: './src/custom/theme-light'
318
- }, {
319
- /* ... */
320
- }]
321
- };
322
- ```
323
-
324
- Use the component namespace:
325
-
326
- ```html
327
- <!-- src/index.html -->
328
- <html>
329
- <body>
330
- <x-theme-dark::button>Submit</theme-dark::button>
331
- <x-theme-light::button>Submit</theme-light::button>
332
- </body>
333
- </html>
334
- ```
335
-
336
- Of course, you can change this folder structure as you prefer according to your project requirements.
337
-
338
- ### Slots
339
-
340
- Your components can inject code in specific slots you define, and then you can fill this content when you use the component.
341
- Find below a simple example.
342
-
343
- Create the component:
344
-
345
- ```html
346
- <!-- src/modal.html -->
347
- <div class="modal">
348
- <div class="modal-header">
349
- <slot:header></slot:header>
350
- </div>
351
- <div class="modal-body">
352
- <slot:body></slot:body>
353
- </div>
354
- <div class="modal-footer">
355
- <slot:footer></slot:footer>
356
- </div>
357
- </div>
358
- ```
359
-
360
- Use the component:
361
-
362
- ```html
363
- <!-- src/index.html -->
364
- <x-modal>
365
- <fill:header>Header content</fill:header>
366
- <fill:body>Body content</fill:body>
367
- <fill:footer>Footer content</fill:footer>
368
- </x-modal>
369
- ```
370
-
371
- Result:
372
-
373
- ```html
374
- <!-- dist/index.html -->
375
- <div class="modal">
376
- <div class="modal-header">
377
- Header content
378
- </div>
379
- <div class="modal-body">
380
- Body content
381
- </div>
382
- <div class="modal-footer">
383
- Footer content
384
- </div>
385
- </div>
386
- ```
387
-
388
- By default, the content is replaced, but you can also prepend or append the content, or keep the default content by not filling the slot.
389
-
390
- Add some default content in the component:
391
-
392
- ```html
393
- <!-- src/modal.html -->
394
- <div class="modal">
395
- <div class="modal-header">
396
- <slot:header>Default header</slot:header>
397
- </div>
398
- <div class="modal-body">
399
- <slot:body>content</slot:body>
400
- </div>
401
- <div class="modal-footer">
402
- <slot:footer>Footer</slot:footer>
403
- </div>
404
- </div>
405
- ```
406
-
407
- ```html
408
- <!-- src/index.html -->
409
- <x-modal>
410
- <fill:body prepend>Prepend body</fill:body>
411
- <fill:footer append>content</fill:footer>
412
- </x-modal>
413
- ```
414
-
415
- Result:
416
-
417
- ```html
418
- <!-- dist/index.html -->
419
- <div class="modal">
420
- <div class="modal-header">
421
- Default header
422
- </div>
423
- <div class="modal-body">
424
- Prepend body content
425
- </div>
426
- <div class="modal-footer">
427
- Footer content
428
- </div>
429
- </div>
430
- ```
431
-
432
- ### Stacks
433
-
434
- You can push content to named stacks which can be rendered somewhere else in another place. This can be particularly useful for specifying any JavaScript or CSS required by your components.
435
-
436
- First of all define a `<stack>` anywhere in your code, for example:
437
-
438
- ```html
439
- <!-- src/index.html -->
440
- <html>
441
- <head>
442
- <stack name="styles"></stack>
443
- </head>
444
- <body>
445
-
446
- <x-modal>
447
- <fill:header>Header content</fill:header>
448
- <fill:body>Body content</fill:body>
449
- <fill:footer>Footer content</fill:footer>
450
- </x-modal>
451
-
452
- <stack name="scripts"></stack>
453
- </body>
454
- </html>
455
- ```
456
-
457
- Then in modal components, or any other child components, you can push content to this stack.
458
-
459
- ```html
460
- <!-- src/modal.html -->
461
- <div class="modal">
462
- <div class="modal-header">
463
- <slot:header></slot:header>
464
- </div>
465
- <div class="modal-body">
466
- <slot:body></slot:body>
467
- </div>
468
- <div class="modal-footer">
469
- <slot:footer></slot:footer>
470
- </div>
471
- </div>
472
-
473
- <push name="styles">
474
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
475
- </push>
476
-
477
- <push name="scripts">
478
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
479
- </push>
480
- ```
481
-
482
- The output will be:
483
-
484
- ```html
485
- <!-- dist/index.html -->
486
- <html>
487
- <head>
488
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
489
- </head>
490
- <body>
491
- <div class="modal">
492
- <div class="modal-header">
493
- Header content
494
- </div>
495
- <div class="modal-body">
496
- Body content
497
- </div>
498
- <div class="modal-footer">
499
- Footer content
500
- </div>
501
- </div>
502
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
503
- </body>
504
- </html>
505
- ```
506
-
507
- The `once` attribute allows you to push content only once per rendering cycle. For example, if you are rendering a given component within a loop, you may wish to only push the JavaScript and CSS the first time the component is rendered.
508
-
509
- Example.
510
-
511
- ```html
512
- <!-- src/modal.html -->
513
- <div class="modal">
514
- <!-- ... -->
515
- </div>
516
-
517
- <!-- The push content will be pushed only once in the stack -->
518
-
519
- <push name="styles" once>
520
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
521
- </push>
522
-
523
- <push name="scripts" once>
524
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
525
- </push>
526
- ```
527
-
528
- By default, the content is pushed in the stack in the given order.
529
- If you would like to prepend content onto the beginning of a stack, you should use the `prepend` attribute:
530
-
531
- ```html
532
- <push name="scripts">
533
- <!-- This will be second -->
534
- <script src="/example.js"></script>
535
- </push>
536
-
537
- <!-- Later... -->
538
-
539
- <push name="scripts" prepend>
540
- <!-- This will be first -->
541
- <script src="/example-2.js"></script>
542
- </push>
543
- ```
544
-
545
- ### Props
546
-
547
- Behind the `props` there is powerful [posthtml-expressions](https://github.com/posthtml/posthtml-expressions) plugin, with feature to pass `props` (locals) via attributes and manipulate them via `<script props>`.
548
-
549
- Let's see how it works with a few examples starting with a basic one.
550
-
551
- Create the component:
552
-
553
- ```html
554
- <!-- src/my-component.html -->
555
- <script props>
556
- module.exports = {
557
- prop: props.prop || 'Default prop value'
558
- }
559
- </script>
560
- <div>
561
- {{ prop }}
562
- </div>
563
- ```
564
-
565
- Use:
566
-
567
- ```html
568
- <x-my-component prop="Hello world!"></x-my-component>
569
- ```
570
-
571
- The output will be:
572
-
573
- ```html
574
- <div>
575
- Hello world!
576
- </div>
577
- ```
578
-
579
- Without passing `prop` via attribute then the output would be `Default prop value`, as shown below.
580
-
581
- Use component without passing prop:
582
-
583
- ```html
584
- <x-my-component></x-my-component>
585
- ```
586
-
587
- The output will be:
588
-
589
- ```html
590
- <div>
591
- Default prop value
592
- </div>
593
- ```
594
-
595
- In the `<script props>` you have access to passed props via object `props`, and you can add any logic you need inside it.
596
-
597
- Create the component:
598
-
599
- ```html
600
- <!-- src/modal.html -->
601
- <script props>
602
- module.exports = {
603
- title: props.title || 'Default title',
604
- size: props.size ? `modal-${props.size}` : '',
605
- items: Array.isArray(props.items) ? props.items.concat(['first', 'second']) : ['first', 'second']
606
- }
607
- </script>
608
- <div class="modal {{ size }}">
609
- <div class="modal-header">
610
- {{ title }}
611
- </div>
612
- <div class="modal-body">
613
- <each loop="item in items"><span>{{ item }}</span></each>
614
- </div>
615
- </div>
616
- ```
617
-
618
- Use:
619
-
620
- ```html
621
- <x-modal size="xl" title="My modal title" items='["third", "fourth"]' class="modal-custom"></x-modal>
622
- ```
623
-
624
- The output will be:
625
-
626
- ```html
627
- <div class="modal modal-custom modal-xl">
628
- <div class="modal-header">
629
- My modal title
630
- </div>
631
- <div class="modal-body">
632
- <span>first</span>
633
- <span>second</span>
634
- <span>third</span>
635
- <span>fourth</span>
636
- </div>
637
- </div>
638
- ```
639
-
640
- You can also notice how the `class` attribute is merged with `class` attribute of the first node. In the next section you will know more about this.
641
-
642
- You can change how attributes are merged with global props defined via options by passing a callback function used by lodash method [mergeWith](https://lodash.com/docs/4.17.15#mergeWith).
643
-
644
- By default, all props are scoped to the component, and are not available to nested components. You can however change this accordingly to your need.
645
- Let's see below example.
646
-
647
- Create a component:
648
-
649
- ```html
650
- <!-- src/child.html -->
651
- <script props>
652
- module.exports = {
653
- prop: props.prop || 'Default prop value'
654
- }
655
- </script>
656
- <div>
657
- Prop in child: {{ prop }}
658
- </div>
659
- ```
660
-
661
- Create another component that use the first one:
662
-
663
-
664
- ```html
665
- <!-- src/parent.html -->
666
- <script props>
667
- module.exports = {
668
- prop: props.prop || 'Default prop value'
669
- }
670
- </script>
671
- <div>
672
- Prop in parent: {{ prop }}
673
- <x-child></x-child>
674
- </div>
675
- ```
676
-
677
- Use:
678
-
679
- ```html
680
- <x-parent prop="My prop"></x-parent>
681
- ```
682
-
683
- The output will be:
684
-
685
- ```html
686
- <div>
687
- Prop in parent: My prop
688
- <div>
689
- Prop in child: Default prop value
690
- </div>
691
- </div>
692
- ```
693
-
694
- As you can see `prop` in `x-child` component are default value and not the one set via `x-parent`. Prepend `aware:` to the attribute name to pass the props to nested components.
695
-
696
-
697
- ```html
698
- <x-parent aware:prop="My prop"></x-parent>
699
- ```
700
-
701
- The output now will be:
702
-
703
- ```html
704
- <div>
705
- Prop in parent: My prop
706
- <div>
707
- Prop in child: My prop
708
- </div>
709
- </div>
710
- ```
711
-
712
- ### Attributes
713
-
714
- You can pass any attributes to your components and this will be added to the first node of your component,
715
- or to the node with an attribute named `attributes`. If you are familiar with VueJS this is the same as so called
716
- [fallthrough attribute](https://vuejs.org/guide/components/attrs.html), or with Laravel Blade is
717
- [component-attributes](https://laravel.com/docs/10.x/blade#component-attributes).
718
-
719
- By default `class` and `style` are merged with existing `class` and `style` attribute.
720
- All others attributes are override by default.
721
- Only attributes defined in [valid-attributes.js](https://github.com/thewebartisan7/posthtml-components/blob/main/src/valid-attributes.js)
722
- or not defined as `props` in the `<script props>`.
723
-
724
- As already seen in basic example:
725
-
726
- ```html
727
- <!-- src/button.html -->
728
- <script props>
729
- module.exports = {
730
- label: props.label || 'A button'
731
- }
732
- </script>
733
- <button type="button" class="btn">
734
- {{ label }}
735
- </button>
736
- ```
737
-
738
- Use the component:
739
-
740
- ```html
741
- <!-- src/index.html -->
742
- <x-button type="submit" class="btn-primary" label="My button"></x-button>
743
- ```
744
-
745
- Result:
746
-
747
- ```html
748
- <!-- dist/index.html -->
749
- <button type="submit" class="btn btn-primary">My button</button>
750
- ```
751
-
752
- As you may notice the `label` attribute is not added as attribute, since it's defined as a `props`.
753
-
754
- As said early, `class` and `style` are merged by default, if you want to override them, just prepend `override:` to the attribute name:
755
-
756
- ```html
757
- <!-- src/index.html -->
758
- <x-button type="submit" override:class="btn-custom" label="My button"></x-button>
759
- ```
760
-
761
- Result:
762
-
763
- ```html
764
- <!-- dist/index.html -->
765
- <button type="submit" class="btn-custom">My button</button>
766
- ```
767
-
768
- If you want to use another node and not the first one, then you can add the attribute `attributes` like shown below.
769
-
770
- ```html
771
- <!-- src/my-component.html -->
772
- <div class="first-node">
773
- <div class="second-node" attributes>
774
- Hello world!
775
- </div>
776
- </div>
777
- ```
778
-
779
- Use the component:
780
-
781
- ```html
782
- <!-- src/index.html -->
783
- <x-my-component class="my-class"></x-my-component>
784
- ```
785
-
786
- Result:
787
-
788
- ```html
789
- <!-- dist/index.html -->
790
- <div class="first-node">
791
- <div class="second-node my-class">
792
- Hello world!
793
- </div>
794
- </div>
795
- ```
796
-
797
- You can add custom rules how attributes are parsed, as behind the scene it's used [posthtml-attrs-parser](https://github.com/posthtml/posthtml-attrs-parser) plugin.
798
-
799
- ### Advanced attributes configurations
800
-
801
- If default configurations for valid attributes are not right for you, then you can configure them as explained below.
802
-
803
- ```js
804
- // index.js
805
- const { readFileSync, writeFileSync } = require('fs')
806
-
807
- const posthtml = require('posthtml')
808
- const components = require('posthtml-components')
809
-
810
- const options = {
811
- root: './src',
812
-
813
- // Add attributes to specific tag or override defaults
814
- elementAttributes: {
815
- DIV: (defaultAttributes) => {
816
- /* Add new one */
817
- defaultAttributes.push('custom-attribute-name');
818
-
819
- return defaultAttributes;
820
- },
821
- DIV: (defaultAttributes) => {
822
- /* Override all */
823
- defaultAttributes = ['custom-attribute-name', 'another-one'];
824
-
825
- return defaultAttributes;
826
- },
827
- },
828
-
829
- // Add attributes to all tags, use '*' as wildcard for attribute name that starts with
830
- safelistAttributes: [
831
- 'custom-attribute-name',
832
- 'attribute-name-start-with-*'
833
- ],
834
-
835
- // Remove attributes from all tags that support it
836
- blacklistAttributes: [
837
- 'role'
838
- ]
839
- }
840
-
841
- posthtml(components(options))
842
- .process(readFileSync('src/index.html', 'utf8'))
843
- .then((result) => writeFileSync('dist/index.html', result.html, 'utf8'))
844
- ```
845
-
846
- ## Examples
847
-
848
- You can work with `<slot>` and `<fill>` or you can create component for each block of your component, and you can also support both of them.
849
- You can find an example of this inside `docs-src/components/modal`. Below is a short explanation about the both approach.
850
-
851
- ### Using slots
852
-
853
- Let's suppose we want to create a component for [bootstrap modal](https://getbootstrap.com/docs/5.2/components/modal/). The code required is:
854
-
855
- ```html
856
- <!-- Modal HTML -->
857
- <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
858
- <div class="modal-dialog">
859
- <div class="modal-content">
860
- <div class="modal-header">
861
- <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
862
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
863
- </div>
864
- <div class="modal-body">
865
- ...
866
- </div>
867
- <div class="modal-footer">
868
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
869
- <button type="button" class="btn btn-primary">Save changes</button>
870
- </div>
871
- </div>
872
- </div>
873
- </div>
874
- ```
875
-
876
- There is almost three block of code: the header, the body and the footer.
877
- So we could create our component with three slot like below:
878
-
879
- ```html
880
- <!-- Modal component -->
881
- <div class="modal fade" tabindex="-1" aria-hidden="true">
882
- <div class="modal-dialog">
883
- <div class="modal-content">
884
- <div class="modal-header">
885
- <slot:header></slot:header>
886
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
887
- </div>
888
- <div class="modal-body">
889
- <slot:body></slot:body>
890
- </div>
891
- <div class="modal-footer">
892
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
893
- <slot:footer></slot:footer>
894
- </div>
895
- </div>
896
- </div>
897
- </div>
898
- ```
899
-
900
- In this case we can use it like:
901
-
902
- ```html
903
- <x-modal
904
- id="exampleModal"
905
- aria-labelledby="exampleModalLabel"
906
- >
907
- <slot:header>
908
- <h5 class="modal-title" id="exampleModalLabel">My modal</h5>
909
- </slot:header>
910
-
911
- <slot:body>
912
- Modal body content goes here...
913
- </slot:body>
914
-
915
- <slot:footer close="false">
916
- <button type="button" class="btn btn-primary">Confirm</button>
917
- </slot:footer>
918
- </x-modal>
919
- ```
920
-
921
- ### Splitting component in small component
922
-
923
- Another way is to split the component in small component, my preferred way, because you can pass attributes to each of them.
924
- So we create the component with a main component and then three different small component:
925
-
926
- ```html
927
- <!-- Main modal component -->
928
- <div class="modal fade" tabindex="-1" aria-hidden="true">
929
- <div class="modal-dialog">
930
- <div class="modal-content">
931
- <yield></yield>
932
- </div>
933
- </div>
934
- </div>
935
- ```
936
-
937
- ```html
938
- <!-- Header modal component -->
939
- <div class="modal-header">
940
- <yield></yield>
941
- </div>
942
- ```
943
-
944
- ```html
945
- <!-- Body modal component -->
946
- <div class="modal-body">
947
- <yield></yield>
948
- </div>
949
- ```
950
-
951
- ```html
952
- <!-- Footer modal component -->
953
- <div class="modal-footer">
954
- <yield></yield>
955
- </div>
956
- ```
957
-
958
- And then you can use it like below example:
959
-
960
- ```html
961
- <x-modal
962
- id="exampleModal"
963
- aria-labelledby="exampleModalLabel"
964
- >
965
- <x-modal.header>
966
- <h5 class="modal-title" id="exampleModalLabel">My modal</h5>
967
- </x-modal.header>
968
-
969
- <x-modal.body>
970
- Modal body content goes here...
971
- </x-modal.body>
972
-
973
- <x-modal.footer>
974
- <button type="button" class="btn btn-primary">Confirm</button>
975
- </x-modal.footer>
976
- </x-modal>
977
- ```
978
-
979
- As said in this way you can pass attributes to each of them, without defining props.
980
-
981
- ### Combine slots and small component
982
-
983
- You can also combine both way, and then use them with slots or with small component:
984
-
985
- ```html
986
-
987
- <!-- Modal -->
988
- <div
989
- class="modal fade"
990
- tabindex="-1"
991
- aria-hidden="true"
992
- aria-modal="true"
993
- role="dialog"
994
- >
995
- <div class="modal-dialog">
996
- <div class="modal-content">
997
- <if condition="$slots.header?.filled">
998
- <x-modal.header>
999
- <slot:header></slot:header>
1000
- </x-modal.header>
1001
- </if>
1002
- <if condition="$slots.body?.filled">
1003
- <x-modal.body>
1004
- <slot:body></slot:body>
1005
- </x-modal.body>
1006
- </if>
1007
- <if condition="$slots.footer?.filled">
1008
- <x-modal.footer close="{{ $slots.footer?.props.close }}">
1009
- <slot:footer></slot:footer>
1010
- </x-modal.footer>
1011
- </if>
1012
- <yield></yield>
1013
- </div>
1014
- </div><!-- /.modal-dialog -->
1015
- </div><!-- /.modal -->
1016
- ```
1017
-
1018
- Now you can use your component with slots or with small components.
1019
- As you may notice, by using slots, you already can use also your small components, and so you can also pass props
1020
- via `$slots` which has all the `props` passed via slot, and as well check if slot is filled.
1021
-
1022
- ## Migration
1023
-
1024
- If you are migrating from `posthtml-extend` and/or `posthtml-modules` please to follow updates here:
1025
- [posthtml-components/issues/16](https://github.com/thewebartisan7/posthtml-components/issues/16).
1026
-
1027
- ## Contributing
1028
-
1029
- See [PostHTML Guidelines](https://github.com/posthtml/posthtml/tree/master/docs) and [contribution guide](CONTRIBUTING.md).
1030
-
1031
- [npm]: https://img.shields.io/npm/v/posthtml-component.svg
1032
- [npm-url]: https://www.npmjs.com/package/posthtml-component
1033
-
1034
- [style]: https://img.shields.io/badge/code_style-XO-5ed9c7.svg
1035
- [style-url]: https://github.com/sindresorhus/xo
1036
-
1037
- [cover]: https://coveralls.io/repos/thewebartisan7/posthtml-components/badge.svg?branch=main
1038
- [cover-badge]: https://coveralls.io/r/thewebartisan7/posthtml-components?branch=main
1039
-
1040
- ## Credits
1041
-
1042
- Thanks to all PostHTML contributors and especially to `posthtml-extend` and `posthtml-modules` contributors, as part of code are ~~stolen~~ inspired from these plugins.
1043
- Huge thanks also to Laravel Blade template engine.
1
+ [npm]: https://www.npmjs.com/package/posthtml-component
2
+ [npm-version-shield]: https://img.shields.io/npm/v/posthtml-component.svg
3
+ [npm-stats]: http://npm-stat.com/charts.html?package=posthtml-component
4
+ [npm-stats-shield]: https://img.shields.io/npm/dt/posthtml-component.svg
5
+ [github-ci]: https://github.com/posthtml/posthtml-components/actions/workflows/build.yml
6
+ [github-ci-shield]: https://github.com/posthtml/posthtml-components/actions/workflows/build.yml/badge.svg
7
+ [license]: ./LICENSE
8
+ [license-shield]: https://img.shields.io/npm/l/posthtml-component.svg
9
+
10
+ <div align="center">
11
+ <img width="150" height="150" alt="PostHTML" src="https://posthtml.github.io/posthtml/logo.svg">
12
+ <h1>PostHTML Components</h1>
13
+ <p>Laravel Blade-inspired components for PostHTML</p>
14
+
15
+ [![Version][npm-version-shield]][npm]
16
+ [![Build][github-ci-shield]][github-ci]
17
+ [![License][license-shield]][license]
18
+ [![Downloads][npm-stats-shield]][npm-stats]
19
+ </div>
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm i -D posthtml-component
25
+ ```
26
+
27
+ ## Introduction
28
+
29
+ This PostHTML plugin provides an HTML-friendly syntax for using components in your HTML templates.
30
+ If you are familiar with Blade, React, Vue or similar, you will find the syntax to be familiar, as this plugin is inspired by them.
31
+
32
+ **See also the first [PostHTML Bootstrap UI](https://github.com/thewebartisan7/posthtml-bootstrap-ui) using this plugin and check also the [starter template here](https://github.com/thewebartisan7/posthtml-bootstrap-ui-starter).**
33
+
34
+ ## Basic example
35
+
36
+ Create the component:
37
+
38
+ ``` html
39
+ <!-- src/button.html -->
40
+ <button type="button" class="btn">
41
+ <yield></yield>
42
+ </button>
43
+ ```
44
+
45
+ Use it:
46
+
47
+ ``` html
48
+ <!-- src/index.html -->
49
+ <html>
50
+ <body>
51
+ <x-button type="submit" class="btn-primary">Submit</x-button>
52
+ </body>
53
+ </html>
54
+ ```
55
+
56
+ Init PostHTML:
57
+
58
+ ```js
59
+ // index.js
60
+ const posthtml = require('posthtml')
61
+ const components = require('posthtml-components')
62
+ const { readFileSync, writeFileSync } = require('node:fs')
63
+
64
+ posthtml([
65
+ components({ root: './src' })
66
+ ])
67
+ .process(readFileSync('src/index.html', 'utf8'))
68
+ .then(result => writeFileSync('dist/index.html', result.html, 'utf8'))
69
+ ```
70
+
71
+ Result:
72
+
73
+ ``` html
74
+ <!-- dist/index.html -->
75
+ <html>
76
+ <body>
77
+ <button type="submit" class="btn btn-primary">Submit</button>
78
+ </body>
79
+ </html>
80
+ ```
81
+
82
+ You might have noticed that the `src/button.html` component contains `type` and `class` attributes, and that we also passed those attributes when we used it in `src/index.html`.
83
+
84
+ The result is that `type` was overridden, and `class` was merged.
85
+
86
+ By default, `class` and `style` attributes are merged, while all others attribute are overridden. You can also override `class` and `style` attributes by prepending `override:` to the class attribute.
87
+
88
+ For example:
89
+
90
+ ```html
91
+ <x-button override:class="btn-custom">Submit</x-button>
92
+
93
+ <!-- Output -->
94
+ <button type="button" class="btn-custom">Submit</button>
95
+ ```
96
+
97
+ All attributes that you pass to the component will be added to the first node of your component or to the node with an attribute named `attributes`, _only_ if they are not defined as `props` via `<script props>` or if they are not "known attributes" (see
98
+ [valid-attributes.js](https://github.com/thewebartisan7/posthtml-components/blob/main/src/valid-attributes.js)).
99
+
100
+ You can also define which attributes are considered to be valid, via the plugin's options.
101
+
102
+ More details on this in [Attributes](#attributes) section.
103
+
104
+ ### yield tag
105
+
106
+ The `<yield></yield>` tag is where content that you pass to a component will be injected.
107
+
108
+ The plugin configures the PostHTML parser to recognize self-closing tags, so you can also just write is as `<yield />`.
109
+
110
+ For brevity, we will use self-closing tags in the examples.
111
+
112
+ ### More examples
113
+
114
+ See also the `docs-src` folder where you can find more examples.
115
+
116
+ You can clone this repo and run `npm run build` to compile them.
117
+
118
+ ## Options
119
+
120
+ | Name | Type | Default | Description |
121
+ |--------------------------|-------------------|----------------------------------------------|-----------------------------------------------------------------------------------------|
122
+ | **root** | `String` | `'./'` | Root path for components lookup. |
123
+ | **folders** | `String[]` | `['']` | Array of paths relative to `options.root` or defined namespaces. |
124
+ | **tagPrefix** | `String` | `'x-'` | Tag prefix. |
125
+ | **tag** | `String\|Boolean` | `false` | Component tag. Use with `options.attribute`. Boolean only `false`. |
126
+ | **attribute** | `String` | `'src'` | Component attribute for setting path. |
127
+ | **namespaces** | `String[]` | `[]` | Array of namespace root paths, fallback paths, and custom override paths. |
128
+ | **namespaceSeparator** | `String` | `'::'` | Namespace separator for tag names. |
129
+ | **fileExtension** | `String` | `'html'` | File extension for component files. |
130
+ | **yield** | `String` | `'yield'` | Tag name for injecting main component content. |
131
+ | **slot** | `String` | `'slot'` | Tag name for slots. |
132
+ | **fill** | `String` | `'fill'` | Tag name for filling slots. |
133
+ | **slotSeparator** | `String` | `':'` | Name separator for `<slot>` and `<fill>` tags. |
134
+ | **push** | `String` | `'push'` | Tag name for `<push>`. |
135
+ | **stack** | `String` | `'stack'` | Tag name for `<stack>`. |
136
+ | **propsScriptAttribute** | `String` | `'props'` | Attribute in `<script props>` for retrieving component props. |
137
+ | **propsContext** | `String` | `'props'` | Name of the object inside the script for processing props. |
138
+ | **propsAttribute** | `String` | `'props'` | Attribute to define props as JSON. |
139
+ | **propsSlot** | `String` | `'props'` | Used to retrieve props passed to slot via `$slots.slotName.props`. |
140
+ | **parserOptions** | `Object` | `{recognizeSelfClosing: true}` | Pass options to `posthtml-parser`. |
141
+ | **expressions** | `Object` | `{}` | Pass options to `posthtml-expressions`. |
142
+ | **plugins** | `Array` | `[]` | PostHTML plugins to apply to every parsed component. |
143
+ | **matcher** | `Object` | `[{tag: options.tagPrefix}]` | Array of objects used to match tags. |
144
+ | **attrsParserRules** | `Object` | `{}` | Additional rules for attributes parser plugin. |
145
+ | **strict** | `Boolean` | `true` | Toggle exception throwing. |
146
+ | **mergeCustomizer** | `Function` | `function` | Callback for lodash `mergeWith` to merge `options.expressions.locals` and props. |
147
+ | **utilities** | `Object` | `{merge: _.mergeWith, template: _.template}` | Utility methods passed to `<script props>`. |
148
+ | **elementAttributes** | `Object` | `{}` | Object with tag names and function modifiers of `valid-attributes.js`. |
149
+ | **safelistAttributes** | `String[]` | `['data-*']` | Array of attribute names to add to default valid attributes. |
150
+ | **blocklistAttributes** | `String[]` | `[]` | Array of attribute names to remove from default valid attributes. |
151
+
152
+ ## Features
153
+
154
+ ### Tag names and x-tags
155
+
156
+ You can use the components in multiple ways, or also a combination of them.
157
+
158
+ If you to use components as 'includes', you may define a tag and src attribute name.
159
+
160
+ Using our previous button component example, we can define the tag and attribute names and then use it in this way:
161
+
162
+ ```hbs
163
+ <!-- src/index.html -->
164
+ <html>
165
+ <body>
166
+ <component src="button.html">Submit</component>
167
+ </body>
168
+ </html>
169
+ ```
170
+
171
+ Init PostHTML:
172
+
173
+ ```js
174
+ // index.js
175
+
176
+ require('posthtml')(
177
+ require('posthtml-components')({
178
+ root: './src',
179
+ tag: 'component',
180
+ attribute: 'src'
181
+ }))
182
+ .process(/* ... */)
183
+ .then(/* ... */)
184
+ ```
185
+
186
+ If you need more control over tag matching, you can pass an array of matcher or single object via `options.matcher` like this:
187
+
188
+ ```js
189
+ // index.js
190
+ const options = {
191
+ root: './src',
192
+ matcher: [{tag: 'a-tag'}, {tag: 'another-one'}, {tag: new RegExp(`^app-`, 'i')}]
193
+ };
194
+
195
+ require('posthtml')(require('posthtml-components')(options))
196
+ .process(/* ... */)
197
+ .then(/* ... */)
198
+ ```
199
+
200
+ With `posthtml-components` you don't need to specify the path name when you are using `x-tag-name` syntax.
201
+
202
+ Setup PostHTML:
203
+
204
+ ```js
205
+ // index.js
206
+ const options = {
207
+ root: './src',
208
+ tagPrefix: 'x-'
209
+ };
210
+
211
+ require('posthtml')(require('posthtml-components')(options))
212
+ .process(/* ... */)
213
+ .then(/* ... */)
214
+ ```
215
+
216
+ Use:
217
+
218
+ ``` html
219
+ <!-- src/index.html -->
220
+ <html>
221
+ <body>
222
+ <x-button>Submit</x-button>
223
+ </body>
224
+ </html>
225
+ ```
226
+
227
+ If your components are in a subfolder then you can use `dot` to access it:
228
+
229
+ ``` html
230
+ <!-- src/components/forms/button.html -->
231
+ <x-forms.button>Submit</x-forms.button>
232
+ ```
233
+
234
+ If your components are in a sub-folder with multiple files, then in order to avoid writing out the main file name you can use `index.html` without specifying it.
235
+
236
+ Here's an example:
237
+
238
+ ``` html
239
+ <!-- src/components/modals/index.html -->
240
+ <x-modal.index>Submit</x-modal.index>
241
+
242
+ <!-- You may omit "index" part since the file is named "index.html" -->
243
+ <x-modal>Submit</x-modal>
244
+ ```
245
+
246
+ #### Parser options
247
+
248
+ You may pass options to `posthtml-parser` via `options.parserOptions`.
249
+
250
+ ```js
251
+ // index.js
252
+ const options = {
253
+ root: './src',
254
+ parserOptions: { decodeEntities: true }
255
+ };
256
+
257
+ require('posthtml')(require('posthtml-components')(options))
258
+ .process('some HTML', options.parserOptions)
259
+ .then(/* ... */)
260
+ ```
261
+
262
+ Important: as you can see, whatever `parserOptions` you pass to the plugin, must also be passed in the `process` method in your code, otherwise your PostHTML build will use `posthtml-parser` defaults and will override anything you've passed to `posthtml-component`.
263
+
264
+ #### Self-closing tags
265
+
266
+ The plugin supports self-closing tags by default, but you need to make sure to enable them in the `process` method in your code too, by passing `recognizeSelfClosing: true` in the options object:
267
+
268
+ ```js
269
+ // index.js
270
+ require('posthtml')(require('posthtml-components')({root: './src'}))
271
+ .process('your HTML...', {recognizeSelfClosing: true})
272
+ .then(/* ... */)
273
+ ```
274
+
275
+ If you don't add this to `process`, PostHTML will use `posthtml-parser` defaults and will not support self-closing component tags. This will result in everything after a self-closing tag not being output.
276
+
277
+ ### Multiple folders
278
+
279
+ You have full control over where your component files exist. Once you set the base root path of your components, you can then set multiple folders.
280
+
281
+ For example if your root is `./src` and then you have several folders where you have your components, for example `./src/components` and `./src/layouts`, you can set up the plugin like below:
282
+
283
+ ```js
284
+ // index.js
285
+ const options = {
286
+ root: './src',
287
+ folders: ['components', 'layouts']
288
+ };
289
+
290
+ require('posthtml')(require('posthtml-components')(options))
291
+ .process(/* ... */)
292
+ .then(/* ... */)
293
+ ```
294
+
295
+ ### Namespaces
296
+
297
+ With namespaces, you can define a top level root path to your components.
298
+
299
+ It can be useful for handling custom themes, where you define a specific top level root with a fallback root for when a component is not found, and a custom root for overriding.
300
+
301
+ This makes it possible to create folder structures like this:
302
+
303
+ - `src` (root folder)
304
+ - `components` (folder for components like modal, button, etc.)
305
+ - `layouts` (folder for layout components like base layout, header, footer, etc.)
306
+ - `theme-dark` (namespace folder for theme-dark)
307
+ - `components` (folder for components for theme dark)
308
+ - `layouts` (folder for layout components for dark theme)
309
+ - `theme-light` (namespace folder for theme-light)
310
+ - `components` (folder for components for light theme)
311
+ - `layouts` (folder for layout components for dark theme)
312
+ - `custom` (custom folder for override your namespace themes)
313
+ - `theme-dark` (custom folder for override dark theme)
314
+ - `components` (folder for override components of theme dark)
315
+ - `layouts` (folder for override layout components of dark theme)
316
+ - `theme-light` (custom folder for override light theme)
317
+ - `components` (folder for override components of theme dark)
318
+ - `layouts` (folder for override layout components of dark theme)
319
+
320
+ And the options would be like:
321
+
322
+ ```js
323
+ // index.js
324
+ const options = {
325
+ // Root for component without namespace
326
+ root: './src',
327
+ // Folders is always appended in 'root' or any defined namespace's folders (base, fallback or custom)
328
+ folders: ['components', 'layouts'],
329
+ namespaces: [{
330
+ // Namespace name will be prepended to tag name (example <x-theme-dark::button>)
331
+ name: 'theme-dark',
332
+ // Root of the namespace
333
+ root: './src/theme-dark',
334
+ // Fallback root when a component is not found in namespace
335
+ fallback: './src',
336
+ // Custom root for overriding, the lookup happens here first
337
+ custom: './src/custom/theme-dark'
338
+ }, {
339
+ // Light theme
340
+ name: 'theme-light',
341
+ root: './src/theme-light',
342
+ fallback: './src',
343
+ custom: './src/custom/theme-light'
344
+ }, {
345
+ /* ... */
346
+ }]
347
+ };
348
+ ```
349
+
350
+ Use the component namespace:
351
+
352
+ ```xml
353
+ <!-- src/index.html -->
354
+ <html>
355
+ <body>
356
+ <x-theme-dark::button>Submit</theme-dark::button>
357
+ <x-theme-light::button>Submit</theme-light::button>
358
+ </body>
359
+ </html>
360
+ ```
361
+
362
+ ### Slots
363
+
364
+ Components may define slots that can be filled with content when used.
365
+
366
+ For example:
367
+
368
+ ```xml
369
+ <!-- src/modal.html -->
370
+ <div class="modal">
371
+ <div class="modal-header">
372
+ <slot:header />
373
+ </div>
374
+ <div class="modal-body">
375
+ <slot:body />
376
+ </div>
377
+ <div class="modal-footer">
378
+ <slot:footer />
379
+ </div>
380
+ </div>
381
+ ```
382
+
383
+ Use the component:
384
+
385
+ ```xml
386
+ <!-- src/index.html -->
387
+ <x-modal>
388
+ <fill:header>Header content</fill:header>
389
+ <fill:body>Body content</fill:body>
390
+ <fill:footer>Footer content</fill:footer>
391
+ </x-modal>
392
+ ```
393
+
394
+ Result:
395
+
396
+ ```html
397
+ <!-- dist/index.html -->
398
+ <div class="modal">
399
+ <div class="modal-header">
400
+ Header content
401
+ </div>
402
+ <div class="modal-body">
403
+ Body content
404
+ </div>
405
+ <div class="modal-footer">
406
+ Footer content
407
+ </div>
408
+ </div>
409
+ ```
410
+
411
+ By default, the slot content is replaced, but you can also prepend or append the content, or keep the default content by not filling the slot.
412
+
413
+ Add some default content in the component:
414
+
415
+ ```xml
416
+ <!-- src/modal.html -->
417
+ <div class="modal">
418
+ <div class="modal-header">
419
+ <slot:header>Default header</slot:header>
420
+ </div>
421
+ <div class="modal-body">
422
+ <slot:body>content</slot:body>
423
+ </div>
424
+ <div class="modal-footer">
425
+ <slot:footer>Footer</slot:footer>
426
+ </div>
427
+ </div>
428
+ ```
429
+
430
+ ```xml
431
+ <!-- src/index.html -->
432
+ <x-modal>
433
+ <fill:body prepend>Prepend body</fill:body>
434
+ <fill:footer append>content</fill:footer>
435
+ </x-modal>
436
+ ```
437
+
438
+ Result:
439
+
440
+ ```html
441
+ <!-- dist/index.html -->
442
+ <div class="modal">
443
+ <div class="modal-header">
444
+ Default header
445
+ </div>
446
+ <div class="modal-body">
447
+ Prepend body content
448
+ </div>
449
+ <div class="modal-footer">
450
+ Footer content
451
+ </div>
452
+ </div>
453
+ ```
454
+
455
+ ### Stacks
456
+
457
+ You may push content to named stacks which can be rendered somewhere else, like in another component. This can be particularly useful for specifying any JavaScript or CSS required by your components.
458
+
459
+ First, add a `<stack>` tag to your HTML:
460
+
461
+ ```diff
462
+ <!-- src/index.html -->
463
+ <html>
464
+ <head>
465
+ + <stack name="styles" />
466
+ </head>
467
+ <body>
468
+ <x-modal>
469
+ <fill:header>Header content</fill:header>
470
+ <fill:body>Body content</fill:body>
471
+ <fill:footer>Footer content</fill:footer>
472
+ </x-modal>
473
+
474
+ + <stack name="scripts" />
475
+ </body>
476
+ </html>
477
+ ```
478
+
479
+ Then, in modal components or any other child components, you can push content to this stack:
480
+
481
+ ```xml
482
+ <!-- src/modal.html -->
483
+ <div class="modal">
484
+ <div class="modal-header">
485
+ <slot:header />
486
+ </div>
487
+ <div class="modal-body">
488
+ <slot:body />
489
+ </div>
490
+ <div class="modal-footer">
491
+ <slot:footer />
492
+ </div>
493
+ </div>
494
+
495
+ <push name="styles">
496
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
497
+ </push>
498
+
499
+ <push name="scripts">
500
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
501
+ </push>
502
+ ```
503
+
504
+ The output will be:
505
+
506
+ ```html
507
+ <!-- dist/index.html -->
508
+ <html>
509
+ <head>
510
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
511
+ </head>
512
+ <body>
513
+ <div class="modal">
514
+ <div class="modal-header">
515
+ Header content
516
+ </div>
517
+ <div class="modal-body">
518
+ Body content
519
+ </div>
520
+ <div class="modal-footer">
521
+ Footer content
522
+ </div>
523
+ </div>
524
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
525
+ </body>
526
+ </html>
527
+ ```
528
+
529
+ The `once` attribute allows you to push content only once per rendering cycle.
530
+
531
+ For example, if you are rendering a given component within a loop, you may wish to only push the JavaScript and CSS the first time the component is rendered.
532
+
533
+ Example.
534
+
535
+ ```html
536
+ <!-- src/modal.html -->
537
+ <div class="modal">
538
+ <!-- ... -->
539
+ </div>
540
+
541
+ <!-- The push content will be pushed only once in the stack -->
542
+
543
+ <push name="styles" once>
544
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet">
545
+ </push>
546
+
547
+ <push name="scripts" once>
548
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"></script>
549
+ </push>
550
+ ```
551
+
552
+ By default, the content is pushed in the stack in the given order. If you would like to prepend content onto the beginning of a stack, you may use the `prepend` attribute:
553
+
554
+ ```html
555
+ <push name="scripts">
556
+ <!-- This will be second -->
557
+ <script src="/example.js"></script>
558
+ </push>
559
+
560
+ <!-- Later... -->
561
+ <push name="scripts" prepend>
562
+ <!-- This will be first -->
563
+ <script src="/example-2.js"></script>
564
+ </push>
565
+ ```
566
+
567
+ ### Props
568
+
569
+ `props` can be passed to components in HTML attributes. To use them in a component, they must be defined in the component's `<script props>` tag.
570
+
571
+ For example:
572
+
573
+ ```html
574
+ <!-- src/alert.html -->
575
+ <script props>
576
+ module.exports = {
577
+ title: props.title || 'Default title'
578
+ }
579
+ </script>
580
+ <div>
581
+ {{ title }}
582
+ </div>
583
+ ```
584
+
585
+ Use:
586
+
587
+ ```html
588
+ <x-alert title="Hello world!" />
589
+ ```
590
+
591
+ The output will be:
592
+
593
+ ```html
594
+ <div>
595
+ Hello world!
596
+ </div>
597
+ ```
598
+
599
+ If no `title` attribute is passed to the component, the default value will be used.
600
+
601
+ ```html
602
+ <x-my-alert />
603
+ ```
604
+
605
+ The output will be:
606
+
607
+ ```html
608
+ <div>
609
+ Default title
610
+ </div>
611
+ ```
612
+
613
+ Inside `<script props>` you have access to passed props via an object named `props`.
614
+
615
+ Here's an example of what you can do with it:
616
+
617
+ ```html
618
+ <!-- src/modal.html -->
619
+ <script props>
620
+ module.exports = {
621
+ title: props.title || 'Default title',
622
+ size: props.size ? `modal-${props.size}` : '',
623
+ items: Array.isArray(props.items) ? props.items.concat(['first', 'second']) : ['first', 'second']
624
+ }
625
+ </script>
626
+ <div class="modal {{ size }}">
627
+ <div class="modal-header">
628
+ {{ title }}
629
+ </div>
630
+ <div class="modal-body">
631
+ <each loop="item in items"><span>{{ item }}</span></each>
632
+ </div>
633
+ </div>
634
+ ```
635
+
636
+ Use:
637
+
638
+ ```html
639
+ <x-modal
640
+ size="xl"
641
+ title="My modal title"
642
+ items='["third", "fourth"]'
643
+ class="modal-custom"
644
+ />
645
+ ```
646
+
647
+ The output will be:
648
+
649
+ ```html
650
+ <div class="modal modal-custom modal-xl">
651
+ <div class="modal-header">
652
+ My modal title
653
+ </div>
654
+ <div class="modal-body">
655
+ <span>first</span>
656
+ <span>second</span>
657
+ <span>third</span>
658
+ <span>fourth</span>
659
+ </div>
660
+ </div>
661
+ ```
662
+
663
+ Notice how the `class` attribute that we passed to the component is merged with `class` attribute value of the first node inside of it.
664
+
665
+ You can change how attributes are merged with global props defined via options, by passing a callback function.
666
+
667
+ By default, all props are scoped to the component, and are not available to nested components. You can however change this accordingly to your need.
668
+
669
+ Create a component:
670
+
671
+ ```html
672
+ <!-- src/child.html -->
673
+ <script props>
674
+ module.exports = {
675
+ title: props.title || 'Default title'
676
+ }
677
+ </script>
678
+ <div>
679
+ Prop in child: {{ title }}
680
+ </div>
681
+ ```
682
+
683
+ Create a `<x-parent>` component that uses `<x-child>`:
684
+
685
+ ```html
686
+ <!-- src/parent.html -->
687
+ <script props>
688
+ module.exports = {
689
+ title: props.title || 'Default title'
690
+ }
691
+ </script>
692
+ <div>
693
+ Prop in parent: {{ title }}
694
+ <x-child />
695
+ </div>
696
+ ```
697
+
698
+ Use it:
699
+
700
+ ```html
701
+ <x-parent title="My title" />
702
+ ```
703
+
704
+ The output will be:
705
+
706
+ ```html
707
+ <div>
708
+ Prop in parent: My title
709
+ <div>
710
+ Prop in child: Default title
711
+ </div>
712
+ </div>
713
+ ```
714
+
715
+ As you can see, `title` in `<x-child>` component renders the default value and not the one set via `<x-parent>`.
716
+
717
+ To change this, we must prepend `aware:` to the attribute name in order to pass the props to nested components.
718
+
719
+ ```html
720
+ <x-parent aware:title="My title" />
721
+ ```
722
+
723
+ The output now will be:
724
+
725
+ ```html
726
+ <div>
727
+ Prop in parent: My title
728
+ <div>
729
+ Prop in child: My title
730
+ </div>
731
+ </div>
732
+ ```
733
+
734
+ ### Attributes
735
+
736
+ You can pass any attributes to your components and they will be added to the first node of your component,
737
+ or to the node with an attribute named `attributes`.
738
+
739
+ If you are familiar with Vue.js, this is the same as so-called
740
+ [fallthrough attribute](https://vuejs.org/guide/components/attrs.html). Or, with Laravel Blade, it's
741
+ [component-attributes](https://laravel.com/docs/11.x/blade#component-attributes).
742
+
743
+ By default, `class` and `style` are merged with existing `class` and `style` attribute. All other attributes are overridden by default.
744
+
745
+ If you pass an attribute that is defined as a `prop`, it will not be added to the component's node.
746
+
747
+ Here's an example:
748
+
749
+ ```html
750
+ <!-- src/button.html -->
751
+ <script props>
752
+ module.exports = {
753
+ label: props.label || 'A button'
754
+ }
755
+ </script>
756
+ <button type="button" class="btn">
757
+ {{ label }}
758
+ </button>
759
+ ```
760
+
761
+ Use the component:
762
+
763
+ ```html
764
+ <!-- src/index.html -->
765
+ <x-button type="submit" class="btn-primary" label="My button" />
766
+ ```
767
+
768
+ Result:
769
+
770
+ ```html
771
+ <!-- dist/index.html -->
772
+ <button type="submit" class="btn btn-primary">My button</button>
773
+ ```
774
+
775
+ If you need to override `class` and `style` attribute values (instead of merging them), just prepend `override:` to the attribute name:
776
+
777
+ ```html
778
+ <!-- src/index.html -->
779
+ <x-button type="submit" override:class="btn-custom" label="My button" />
780
+ ```
781
+
782
+ Result:
783
+
784
+ ```html
785
+ <!-- dist/index.html -->
786
+ <button type="submit" class="btn-custom">My button</button>
787
+ ```
788
+
789
+ If you want the attributes to be passed to a certain node, use the `attributes` attribute:
790
+
791
+ ```html
792
+ <!-- src/my-component.html -->
793
+ <div class="first-node">
794
+ <div class="second-node" attributes>
795
+ Hello world!
796
+ </div>
797
+ </div>
798
+ ```
799
+
800
+ Use the component:
801
+
802
+ ```html
803
+ <!-- src/index.html -->
804
+ <x-my-component class="my-class" />
805
+ ```
806
+
807
+ Result:
808
+
809
+ ```html
810
+ <!-- dist/index.html -->
811
+ <div class="first-node">
812
+ <div class="second-node my-class">
813
+ Hello world!
814
+ </div>
815
+ </div>
816
+ ```
817
+
818
+ You can add custom rules to define how attributes are parsed - we use [posthtml-attrs-parser](https://github.com/posthtml/posthtml-attrs-parser) to handle them.
819
+
820
+ ### Advanced attributes configurations
821
+
822
+ If default configurations for valid attributes are not right for you, you may configure them as explained below.
823
+
824
+ ```js
825
+ // index.js
826
+ const { readFileSync, writeFileSync } = require('fs')
827
+
828
+ const posthtml = require('posthtml')
829
+ const components = require('posthtml-components')
830
+
831
+ const options = {
832
+ root: './src',
833
+ // Add attributes to specific tag or override defaults
834
+ elementAttributes: {
835
+ DIV: (defaultAttributes) => {
836
+ /* Add new one */
837
+ defaultAttributes.push('custom-attribute-name');
838
+
839
+ return defaultAttributes;
840
+ },
841
+ DIV: (defaultAttributes) => {
842
+ /* Override all */
843
+ defaultAttributes = ['custom-attribute-name', 'another-one'];
844
+
845
+ return defaultAttributes;
846
+ },
847
+ },
848
+
849
+ // Add attributes to all tags, use '*' as wildcard for attribute name that starts with
850
+ safelistAttributes: [
851
+ 'custom-attribute-name',
852
+ 'attribute-name-start-with-*'
853
+ ],
854
+
855
+ // Remove attributes from all tags that support it
856
+ blocklistAttributes: [
857
+ 'role'
858
+ ]
859
+ }
860
+
861
+ posthtml(components(options))
862
+ .process(readFileSync('src/index.html', 'utf8'))
863
+ .then(result => writeFileSync('dist/index.html', result.html, 'utf8'))
864
+ ```
865
+
866
+ ## Examples
867
+
868
+ You can work with `<slot>` and `<fill>` or you can create component for each block of your component, and you can also support both of them.
869
+
870
+ You can find an example of this inside `docs-src/components/modal`. Following is a short explanation of both approaches.
871
+
872
+ ### Using slots
873
+
874
+ Let's suppose we want to create a component for [bootstrap modal](https://getbootstrap.com/docs/5.2/components/modal/).
875
+
876
+ The code required is:
877
+
878
+ ```html
879
+ <!-- Modal HTML -->
880
+ <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
881
+ <div class="modal-dialog">
882
+ <div class="modal-content">
883
+ <div class="modal-header">
884
+ <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
885
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
886
+ </div>
887
+ <div class="modal-body">
888
+ ...
889
+ </div>
890
+ <div class="modal-footer">
891
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
892
+ <button type="button" class="btn btn-primary">Save changes</button>
893
+ </div>
894
+ </div>
895
+ </div>
896
+ </div>
897
+ ```
898
+
899
+ There is almost three block of code: the header, the body and the footer.
900
+
901
+ So we could create a component with three slots:
902
+
903
+ ```diff
904
+ <!-- Modal component -->
905
+ <div class="modal fade" tabindex="-1" aria-hidden="true">
906
+ <div class="modal-dialog">
907
+ <div class="modal-content">
908
+ <div class="modal-header">
909
+ - <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
910
+ + <slot:header />
911
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
912
+ </div>
913
+ <div class="modal-body">
914
+ + <slot:body />
915
+ </div>
916
+ <div class="modal-footer">
917
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
918
+ + <slot:footer />
919
+ - <button type="button" class="btn btn-primary">Save changes</button>
920
+ </div>
921
+ </div>
922
+ </div>
923
+ </div>
924
+ ```
925
+
926
+ We can then use it like this:
927
+
928
+ ```html
929
+ <x-modal
930
+ id="exampleModal"
931
+ aria-labelledby="exampleModalLabel"
932
+ >
933
+ <slot:header>
934
+ <h5 class="modal-title" id="exampleModalLabel">My modal</h5>
935
+ </slot:header>
936
+
937
+ <slot:body>
938
+ Modal body content goes here...
939
+ </slot:body>
940
+
941
+ <slot:footer close="false">
942
+ <button type="button" class="btn btn-primary">Confirm</button>
943
+ </slot:footer>
944
+ </x-modal>
945
+ ```
946
+
947
+ ### Splitting component in small component
948
+
949
+ Another approach is to split the component in smaller components, passing attributes to each of them.
950
+
951
+ So we create a main component and then three different smaller components:
952
+
953
+ ```html
954
+ <!-- Main modal component -->
955
+ <div class="modal fade" tabindex="-1" aria-hidden="true">
956
+ <div class="modal-dialog">
957
+ <div class="modal-content">
958
+ <yield />
959
+ </div>
960
+ </div>
961
+ </div>
962
+ ```
963
+
964
+ ```html
965
+ <!-- Header modal component -->
966
+ <div class="modal-header">
967
+ <yield />
968
+ </div>
969
+ ```
970
+
971
+ ```html
972
+ <!-- Body modal component -->
973
+ <div class="modal-body">
974
+ <yield />
975
+ </div>
976
+ ```
977
+
978
+ ```html
979
+ <!-- Footer modal component -->
980
+ <div class="modal-footer">
981
+ <yield />
982
+ </div>
983
+ ```
984
+
985
+ And then you can use it like this:
986
+
987
+ ```html
988
+ <x-modal
989
+ id="exampleModal"
990
+ aria-labelledby="exampleModalLabel"
991
+ >
992
+ <x-modal.header>
993
+ <h5 class="modal-title" id="exampleModalLabel">My modal</h5>
994
+ </x-modal.header>
995
+
996
+ <x-modal.body>
997
+ Modal body content goes here...
998
+ </x-modal.body>
999
+
1000
+ <x-modal.footer>
1001
+ <button type="button" class="btn btn-primary">Confirm</button>
1002
+ </x-modal.footer>
1003
+ </x-modal>
1004
+ ```
1005
+
1006
+ As said in this way you can pass attributes to each of them, without defining props.
1007
+
1008
+ ### Combine slots and small component
1009
+
1010
+ You can also combine both approaches, and then use them with slots or with small components:
1011
+
1012
+ ```html
1013
+
1014
+ <!-- Modal -->
1015
+ <div
1016
+ class="modal fade"
1017
+ tabindex="-1"
1018
+ aria-hidden="true"
1019
+ aria-modal="true"
1020
+ role="dialog"
1021
+ >
1022
+ <div class="modal-dialog">
1023
+ <div class="modal-content">
1024
+ <if condition="$slots.header?.filled">
1025
+ <x-modal.header>
1026
+ <slot:header />
1027
+ </x-modal.header>
1028
+ </if>
1029
+ <if condition="$slots.body?.filled">
1030
+ <x-modal.body>
1031
+ <slot:body />
1032
+ </x-modal.body>
1033
+ </if>
1034
+ <if condition="$slots.footer?.filled">
1035
+ <x-modal.footer close="{{ $slots.footer?.props.close }}">
1036
+ <slot:footer />
1037
+ </x-modal.footer>
1038
+ </if>
1039
+ <yield />
1040
+ </div>
1041
+ </div><!-- /.modal-dialog -->
1042
+ </div><!-- /.modal -->
1043
+ ```
1044
+
1045
+ Now you can use your component with slots or with small components.
1046
+
1047
+ As you may notice, by using slots, you already can use also your small components, and so you can also pass props
1048
+ via `$slots` which has all the `props` passed via slot, and as well check if slot is filled.
1049
+
1050
+ ## Migration
1051
+
1052
+ If you are migrating from `posthtml-extend` and/or `posthtml-modules` please to follow updates here:
1053
+ [posthtml-components/issues/16](https://github.com/thewebartisan7/posthtml-components/issues/16).
1054
+
1055
+ ## Contributing
1056
+
1057
+ See [PostHTML Guidelines](https://github.com/posthtml/posthtml/tree/master/docs) and [contribution guide](CONTRIBUTING.md).
1058
+
1059
+ ## Credits
1060
+
1061
+ Thanks to all PostHTML contributors and especially to `posthtml-extend` and `posthtml-modules` contributors, as part of code is ~~stolen~~ inspired from these plugins.
1062
+ Huge thanks also to Laravel Blade template engine.