@diniz/webcomponents 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +563 -750
- package/dist/README.md +563 -750
- package/dist/button-demo-BcfxxPSq.js +227 -0
- package/dist/card-demo-Cxp-wRGW.js +230 -0
- package/dist/date-picker-demo-B8y3zapN.js +143 -0
- package/dist/form-demo-page-F1iLCgfh.js +351 -0
- package/dist/home-page-XUM8cHP7.js +468 -0
- package/dist/index-DiYekJaQ.js +2424 -0
- package/dist/layout-demo-CJsZ6DI5.js +289 -0
- package/dist/modal-demo-page-YN2KgJ31.js +195 -0
- package/dist/stepper-demo-page-BkcpKk_F.js +312 -0
- package/dist/table-demo-x2ZD8cFh.js +137 -0
- package/dist/tabs-demo-BQBtZzw9.js +76 -0
- package/dist/toast-demo-page-DLVacHXA.js +260 -0
- package/dist/webcomponents.es.js +31 -1883
- package/dist/webcomponents.umd.js +2537 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,948 +12,761 @@ A lightweight, framework-agnostic web components library built with vanilla Type
|
|
|
12
12
|
🎯 **Tree-shakeable** - Import only what you need
|
|
13
13
|
♿ **Accessible** - ARIA attributes and keyboard navigation
|
|
14
14
|
|
|
15
|
-
## 🚀 Live Demo
|
|
15
|
+
## 🚀 Live Demo & Component Documentation
|
|
16
16
|
|
|
17
|
-
Check out the interactive demo and component
|
|
17
|
+
Check out the interactive demo and explore component implementations:
|
|
18
18
|
|
|
19
19
|
**[View Live Demo →](https://rodiniz.github.io/webcomponents/)**
|
|
20
20
|
|
|
21
|
+
**Component source code and demos are located in the `src/features/` directory:**
|
|
22
|
+
- Button Demo: `src/features/button-demo/`
|
|
23
|
+
- Input Demo: `src/features/input-demo/`
|
|
24
|
+
- Table Demo: `src/features/table-demo/`
|
|
25
|
+
- Form Demo: `src/features/form-demo/`
|
|
26
|
+
- Date Picker Demo: `src/features/date-picker-demo/`
|
|
27
|
+
- And more...
|
|
28
|
+
|
|
29
|
+
Each demo includes the component implementation and usage examples. Visit the live demo site to see all components in action.
|
|
30
|
+
|
|
21
31
|
## Installation
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
34
|
npm install @diniz/webcomponents
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
##
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import '@diniz/webcomponents';
|
|
41
|
+
import '@diniz/webcomponents/dist/style.css';
|
|
42
|
+
|
|
43
|
+
// Components are now available
|
|
44
|
+
document.body.innerHTML = `
|
|
45
|
+
<ui-button variant="primary">Click Me</ui-button>
|
|
46
|
+
<ui-date-picker format="DD/MM/YYYY"></ui-date-picker>
|
|
47
|
+
`;
|
|
48
|
+
```
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
## Quick Start with Vite (No Framework)
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
Create a new Vite project without any framework to use these web components:
|
|
53
|
+
|
|
54
|
+
### 1. Create a new Vite project
|
|
32
55
|
|
|
33
56
|
```bash
|
|
34
|
-
# Create a new Vite project with vanilla TypeScript template
|
|
35
57
|
npm create vite@latest my-app -- --template vanilla-ts
|
|
36
58
|
cd my-app
|
|
37
|
-
npm install
|
|
38
59
|
```
|
|
39
60
|
|
|
40
|
-
### 2. Install the
|
|
61
|
+
### 2. Install the web components library
|
|
41
62
|
|
|
42
63
|
```bash
|
|
43
64
|
npm install @diniz/webcomponents
|
|
44
65
|
```
|
|
45
66
|
|
|
46
|
-
### 3. Import
|
|
47
|
-
|
|
48
|
-
In your `src/main.ts` file:
|
|
67
|
+
### 3. Import components in your `src/main.ts`
|
|
49
68
|
|
|
50
69
|
```typescript
|
|
51
70
|
import '@diniz/webcomponents';
|
|
52
|
-
import '@diniz/webcomponents/dist/style.css';
|
|
71
|
+
import '@diniz/webcomponents/dist/style.css';
|
|
72
|
+
import './style.css';
|
|
53
73
|
|
|
54
|
-
// Now you can use the components in your HTML
|
|
55
74
|
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
|
56
75
|
<div>
|
|
57
76
|
<h1>My Web Components App</h1>
|
|
58
|
-
|
|
59
|
-
<ui-
|
|
77
|
+
|
|
78
|
+
<ui-button variant="primary">Primary Button</ui-button>
|
|
79
|
+
<ui-button variant="secondary">Secondary Button</ui-button>
|
|
80
|
+
|
|
81
|
+
<ui-input
|
|
82
|
+
label="Email"
|
|
83
|
+
type="email"
|
|
84
|
+
placeholder="Enter your email"
|
|
85
|
+
required
|
|
86
|
+
></ui-input>
|
|
87
|
+
|
|
88
|
+
<ui-date-picker
|
|
89
|
+
label="Select Date"
|
|
90
|
+
format="DD/MM/YYYY"
|
|
91
|
+
></ui-date-picker>
|
|
60
92
|
</div>
|
|
61
93
|
`;
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### 4. Use Components in HTML
|
|
65
94
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<!DOCTYPE html>
|
|
70
|
-
<html lang="en">
|
|
71
|
-
<head>
|
|
72
|
-
<meta charset="UTF-8" />
|
|
73
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
74
|
-
<title>My App</title>
|
|
75
|
-
</head>
|
|
76
|
-
<body>
|
|
77
|
-
<div id="app">
|
|
78
|
-
<ui-button variant="primary">Click Me</ui-button>
|
|
79
|
-
<ui-date-picker format="DD/MM/YYYY"></ui-date-picker>
|
|
80
|
-
<ui-table></ui-table>
|
|
81
|
-
</div>
|
|
82
|
-
<script type="module" src="/src/main.ts"></script>
|
|
83
|
-
</body>
|
|
84
|
-
</html>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 5. Add Event Listeners (Optional)
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
// Wait for components to be defined
|
|
91
|
-
customElements.whenDefined('ui-button').then(() => {
|
|
92
|
-
const button = document.querySelector('ui-button');
|
|
93
|
-
button?.addEventListener('click', () => {
|
|
94
|
-
console.log('Button clicked!');
|
|
95
|
-
});
|
|
95
|
+
// Listen to component events
|
|
96
|
+
document.querySelector('ui-button')?.addEventListener('click', () => {
|
|
97
|
+
console.log('Button clicked!');
|
|
96
98
|
});
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}) as EventListener);
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### 6. TypeScript Support
|
|
106
|
-
|
|
107
|
-
For full TypeScript support, create a `src/types.d.ts` file:
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
declare module '@diniz/webcomponents' {
|
|
111
|
-
export interface UIButton extends HTMLElement {
|
|
112
|
-
variant: 'primary' | 'secondary' | 'ghost';
|
|
113
|
-
size: 'sm' | 'md' | 'lg';
|
|
114
|
-
icon?: string;
|
|
115
|
-
disabled?: boolean;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface UIDatePicker extends HTMLElement {
|
|
119
|
-
format: string;
|
|
120
|
-
value: string;
|
|
121
|
-
min?: string;
|
|
122
|
-
max?: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Add other component interfaces as needed
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
declare global {
|
|
129
|
-
interface HTMLElementTagNameMap {
|
|
130
|
-
'ui-button': import('@diniz/webcomponents').UIButton;
|
|
131
|
-
'ui-date-picker': import('@diniz/webcomponents').UIDatePicker;
|
|
132
|
-
// Add other components as needed
|
|
133
|
-
}
|
|
134
|
-
}
|
|
100
|
+
document.querySelector('ui-input')?.addEventListener('input', (e: Event) => {
|
|
101
|
+
const input = e.target as HTMLInputElement;
|
|
102
|
+
console.log('Input value:', input.value);
|
|
103
|
+
});
|
|
135
104
|
```
|
|
136
105
|
|
|
137
|
-
###
|
|
106
|
+
### 4. Run the development server
|
|
138
107
|
|
|
139
108
|
```bash
|
|
140
|
-
npm run
|
|
109
|
+
npm run dev
|
|
141
110
|
```
|
|
142
111
|
|
|
143
|
-
|
|
112
|
+
Your app is now running with web components! Open your browser and start building.
|
|
144
113
|
|
|
145
|
-
###
|
|
114
|
+
### Example: Building a Counter with Signals
|
|
146
115
|
|
|
147
|
-
|
|
116
|
+
Create reactive components using the signals system:
|
|
148
117
|
|
|
118
|
+
**src/components/counter.ts**
|
|
149
119
|
```typescript
|
|
150
|
-
|
|
151
|
-
import { UIButton } from '@diniz/webcomponents';
|
|
152
|
-
import '@diniz/webcomponents/dist/style.css';
|
|
120
|
+
import { BaseComponent } from '@diniz/webcomponents';
|
|
153
121
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
122
|
+
class CounterComponent extends BaseComponent {
|
|
123
|
+
private count = this.useSignal(0);
|
|
124
|
+
|
|
125
|
+
connectedCallback() {
|
|
126
|
+
super.connectedCallback();
|
|
127
|
+
this.render();
|
|
128
|
+
}
|
|
159
129
|
|
|
160
|
-
|
|
130
|
+
private increment() {
|
|
131
|
+
this.count.set(this.count.get() + 1);
|
|
132
|
+
}
|
|
161
133
|
|
|
162
|
-
|
|
134
|
+
private decrement() {
|
|
135
|
+
this.count.set(this.count.get() - 1);
|
|
136
|
+
}
|
|
163
137
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
138
|
+
render() {
|
|
139
|
+
this.shadowRoot!.innerHTML = `
|
|
140
|
+
<style>
|
|
141
|
+
:host {
|
|
142
|
+
display: block;
|
|
143
|
+
padding: 2rem;
|
|
144
|
+
text-align: center;
|
|
145
|
+
}
|
|
146
|
+
.count {
|
|
147
|
+
font-size: 3rem;
|
|
148
|
+
margin: 1rem 0;
|
|
149
|
+
color: var(--color-primary, #24ec71);
|
|
150
|
+
}
|
|
151
|
+
.buttons {
|
|
152
|
+
display: flex;
|
|
153
|
+
gap: 1rem;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
|
|
158
|
+
<div>
|
|
159
|
+
<h2>Counter</h2>
|
|
160
|
+
<div class="count">${this.count.get()}</div>
|
|
161
|
+
<div class="buttons">
|
|
162
|
+
<ui-button id="decrement" variant="secondary">-</ui-button>
|
|
163
|
+
<ui-button id="increment" variant="primary">+</ui-button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
this.shadowRoot!.getElementById('increment')?.addEventListener('click',
|
|
169
|
+
() => this.increment()
|
|
170
|
+
);
|
|
171
|
+
this.shadowRoot!.getElementById('decrement')?.addEventListener('click',
|
|
172
|
+
() => this.decrement()
|
|
173
|
+
);
|
|
174
|
+
}
|
|
176
175
|
}
|
|
176
|
+
|
|
177
|
+
customElements.define('my-counter', CounterComponent);
|
|
177
178
|
```
|
|
178
|
-
|
|
179
|
+
|
|
180
|
+
**src/main.ts**
|
|
179
181
|
```typescript
|
|
180
182
|
import '@diniz/webcomponents';
|
|
181
183
|
import '@diniz/webcomponents/dist/style.css';
|
|
184
|
+
import './components/counter';
|
|
185
|
+
import './style.css';
|
|
182
186
|
|
|
183
187
|
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
|
184
188
|
<div>
|
|
185
189
|
<h1>My Web Components App</h1>
|
|
186
|
-
<
|
|
187
|
-
<ui-date-picker format="DD/MM/YYYY"></ui-date-picker>
|
|
188
|
-
<ui-table id="myTable"></ui-table>
|
|
190
|
+
<my-counter></my-counter>
|
|
189
191
|
</div>
|
|
190
192
|
`;
|
|
191
|
-
|
|
192
|
-
// Add some data to the table
|
|
193
|
-
const table = document.getElementById('myTable') as any;
|
|
194
|
-
table.data = {
|
|
195
|
-
columns: [
|
|
196
|
-
{ key: 'name', label: 'Name' },
|
|
197
|
-
{ key: 'role', label: 'Role' }
|
|
198
|
-
],
|
|
199
|
-
rows: [
|
|
200
|
-
{ name: 'Alice', role: 'Admin' },
|
|
201
|
-
{ name: 'Bob', role: 'User' }
|
|
202
|
-
]
|
|
203
|
-
};
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Using via CDN or Direct Import
|
|
207
|
-
|
|
208
|
-
```html
|
|
209
|
-
<script type="module">
|
|
210
|
-
import '@diniz/webcomponents';
|
|
211
|
-
</script>
|
|
212
|
-
|
|
213
|
-
<ui-button variant="primary">Click Me</ui-button>
|
|
214
|
-
<ui-date-picker format="DD/MM/YYYY"></ui-date-picker>
|
|
215
|
-
<ui-table></ui-table>
|
|
216
193
|
```
|
|
217
194
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
### 🔘 Button (`ui-button`)
|
|
221
|
-
|
|
222
|
-
A versatile button component with multiple variants, sizes, and icon support.
|
|
195
|
+
### Adding Routing to Your App
|
|
223
196
|
|
|
224
|
-
|
|
225
|
-
- 3 variants: `primary`, `secondary`, `ghost`
|
|
226
|
-
- 3 sizes: `sm`, `md`, `lg`
|
|
227
|
-
- Icon support with [Feather Icons](https://feathericons.com/)
|
|
228
|
-
- Icon positioning (left/right)
|
|
229
|
-
- Icon-only buttons
|
|
230
|
-
- Disabled state support
|
|
231
|
-
- Button type support
|
|
232
|
-
- Smooth transitions and hover effects
|
|
197
|
+
The library includes a built-in router for client-side navigation. Here's how to set it up:
|
|
233
198
|
|
|
234
|
-
|
|
235
|
-
```html
|
|
236
|
-
<ui-button variant="primary" size="md">Primary Button</ui-button>
|
|
237
|
-
<ui-button variant="secondary" size="sm">Secondary</ui-button>
|
|
238
|
-
<ui-button variant="ghost" disabled>Disabled</ui-button>
|
|
239
|
-
|
|
240
|
-
<!-- With icons -->
|
|
241
|
-
<ui-button variant="primary" icon="check">Save</ui-button>
|
|
242
|
-
<ui-button variant="secondary" icon="trash-2" icon-position="right">Delete</ui-button>
|
|
243
|
-
<ui-button variant="ghost" icon="settings"></ui-button>
|
|
244
|
-
```
|
|
199
|
+
#### 1. Create your route configuration
|
|
245
200
|
|
|
246
|
-
**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
- `icon` - Icon name from Feather Icons
|
|
250
|
-
- `icon-position` - Icon position (`left` | `right`, default: `left`)
|
|
251
|
-
- `disabled` - Disable the button
|
|
252
|
-
- `type` - Button type (`button` | `submit` | `reset`)
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
### 📅 Date Picker (`ui-date-picker`)
|
|
201
|
+
**src/router.ts**
|
|
202
|
+
```typescript
|
|
203
|
+
import { createRouter, type Route } from '@diniz/webcomponents';
|
|
257
204
|
|
|
258
|
-
|
|
205
|
+
export const routes: Route[] = [
|
|
206
|
+
{
|
|
207
|
+
path: '/',
|
|
208
|
+
load: () => import('./pages/home'),
|
|
209
|
+
component: 'home-page'
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
path: '/about',
|
|
213
|
+
load: () => import('./pages/about'),
|
|
214
|
+
component: 'about-page'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
path: '/counter',
|
|
218
|
+
load: () => import('./components/counter'),
|
|
219
|
+
component: 'my-counter'
|
|
220
|
+
}
|
|
221
|
+
];
|
|
259
222
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
- Min/max date constraints
|
|
264
|
-
- Text input with format validation
|
|
265
|
-
- Real-time format conversion
|
|
266
|
-
- Disabled state support
|
|
267
|
-
- Custom events for date changes
|
|
223
|
+
// Initialize the router with your routes
|
|
224
|
+
// The router automatically sets up navigation and loads the initial route
|
|
225
|
+
createRouter(routes);
|
|
268
226
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
<ui-date-picker
|
|
272
|
-
format="DD/MM/YYYY"
|
|
273
|
-
value="2026-02-26"
|
|
274
|
-
min="2026-01-01"
|
|
275
|
-
max="2026-12-31"
|
|
276
|
-
></ui-date-picker>
|
|
277
|
-
|
|
278
|
-
<script>
|
|
279
|
-
const picker = document.querySelector('ui-date-picker');
|
|
280
|
-
|
|
281
|
-
picker.addEventListener('date-change', (e) => {
|
|
282
|
-
console.log('ISO:', e.detail.value);
|
|
283
|
-
console.log('Formatted:', e.detail.formattedValue);
|
|
284
|
-
});
|
|
285
|
-
</script>
|
|
227
|
+
// Optional: specify a custom app container selector (default is '#app')
|
|
228
|
+
// createRouter(routes, '#my-app-container');
|
|
286
229
|
```
|
|
287
230
|
|
|
288
|
-
**
|
|
289
|
-
- `format` - Date display format
|
|
290
|
-
- `value` - Date value in ISO format (YYYY-MM-DD)
|
|
291
|
-
- `min` - Minimum date (ISO format)
|
|
292
|
-
- `max` - Maximum date (ISO format)
|
|
293
|
-
- `disabled` - Disable the picker
|
|
294
|
-
- `placeholder` - Placeholder text
|
|
295
|
-
|
|
296
|
-
**Methods:**
|
|
297
|
-
- `getISOValue()` - Get date in ISO format
|
|
298
|
-
- `getFormattedValue()` - Get date in display format
|
|
299
|
-
- `setValue(isoDate)` - Set the date value
|
|
300
|
-
- `clear()` - Clear the date
|
|
231
|
+
> **How it works:** The `createRouter()` function sets up the routing system, registers event listeners for navigation, and automatically loads the initial route when the page loads. Navigation happens via links with the `data-link` attribute, and the browser's back/forward buttons work automatically.
|
|
301
232
|
|
|
302
|
-
**
|
|
303
|
-
- `date-change` - Fired when date changes
|
|
304
|
-
- `date-input` - Fired during input
|
|
233
|
+
**Optional: Adding Route Guards**
|
|
305
234
|
|
|
306
|
-
|
|
235
|
+
You can protect routes with guard functions that return a boolean or a Promise:
|
|
307
236
|
|
|
308
|
-
|
|
237
|
+
```typescript
|
|
238
|
+
import { createRouter, type Route } from '@diniz/webcomponents';
|
|
309
239
|
|
|
310
|
-
|
|
240
|
+
// Example: Synchronous guard
|
|
241
|
+
const isAuthenticated = () => {
|
|
242
|
+
return localStorage.getItem('user') !== null;
|
|
243
|
+
};
|
|
311
244
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
245
|
+
// Example: Async guard (e.g., checking with an API)
|
|
246
|
+
const hasPermission = async () => {
|
|
247
|
+
const response = await fetch('/api/check-permission');
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
return data.hasAccess;
|
|
250
|
+
};
|
|
318
251
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
252
|
+
export const routes: Route[] = [
|
|
253
|
+
{
|
|
254
|
+
path: '/',
|
|
255
|
+
load: () => import('./pages/home'),
|
|
256
|
+
component: 'home-page'
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
path: '/profile',
|
|
260
|
+
load: () => import('./pages/profile'),
|
|
261
|
+
component: 'profile-page',
|
|
262
|
+
guard: isAuthenticated // Redirect to home if guard returns false
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
path: '/admin',
|
|
266
|
+
load: () => import('./pages/admin'),
|
|
267
|
+
component: 'admin-page',
|
|
268
|
+
guard: hasPermission // Supports async guards
|
|
269
|
+
}
|
|
270
|
+
];
|
|
322
271
|
|
|
323
|
-
|
|
324
|
-
const table = document.getElementById('myTable');
|
|
325
|
-
|
|
326
|
-
table.data = {
|
|
327
|
-
columns: [
|
|
328
|
-
{ key: 'name', label: 'Name' },
|
|
329
|
-
{ key: 'role', label: 'Role' },
|
|
330
|
-
{ key: 'score', label: 'Score', align: 'right' }
|
|
331
|
-
],
|
|
332
|
-
rows: [
|
|
333
|
-
{ name: 'Alice', role: 'Admin', score: 95 },
|
|
334
|
-
{ name: 'Bob', role: 'User', score: 87 }
|
|
335
|
-
]
|
|
336
|
-
};
|
|
337
|
-
</script>
|
|
272
|
+
createRouter(routes);
|
|
338
273
|
```
|
|
339
274
|
|
|
340
|
-
|
|
341
|
-
- `data` - Object with `columns` and `rows`
|
|
342
|
-
- `columns`: Array of `{ key, label, align? }`
|
|
343
|
-
- `rows`: Array of objects matching column keys
|
|
275
|
+
#### 2. Create page components (with optional shared navigation)
|
|
344
276
|
|
|
345
|
-
|
|
277
|
+
You can create a reusable navigation component:
|
|
346
278
|
|
|
347
|
-
|
|
279
|
+
**src/components/nav.ts**
|
|
280
|
+
```typescript
|
|
281
|
+
import { BaseComponent } from '@diniz/webcomponents';
|
|
348
282
|
|
|
349
|
-
|
|
283
|
+
class NavComponent extends BaseComponent {
|
|
284
|
+
connectedCallback() {
|
|
285
|
+
super.connectedCallback();
|
|
286
|
+
this.render();
|
|
287
|
+
}
|
|
350
288
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
289
|
+
render() {
|
|
290
|
+
this.shadowRoot!.innerHTML = `
|
|
291
|
+
<style>
|
|
292
|
+
nav {
|
|
293
|
+
background: var(--color-surface, #1e1e1e);
|
|
294
|
+
padding: 1rem;
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: 1rem;
|
|
297
|
+
margin-bottom: 2rem;
|
|
298
|
+
}
|
|
299
|
+
nav a {
|
|
300
|
+
color: var(--color-text, #fff);
|
|
301
|
+
text-decoration: none;
|
|
302
|
+
padding: 0.5rem 1rem;
|
|
303
|
+
border-radius: 0.25rem;
|
|
304
|
+
}
|
|
305
|
+
nav a:hover {
|
|
306
|
+
background: var(--color-surface-hover, #2a2a2a);
|
|
307
|
+
}
|
|
308
|
+
</style>
|
|
309
|
+
|
|
310
|
+
<nav>
|
|
311
|
+
<a href="/" data-link>Home</a>
|
|
312
|
+
<a href="/about" data-link>About</a>
|
|
313
|
+
<a href="/counter" data-link>Counter</a>
|
|
314
|
+
</nav>
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
359
318
|
|
|
360
|
-
|
|
361
|
-
```html
|
|
362
|
-
<ui-pagination
|
|
363
|
-
total="250"
|
|
364
|
-
current-page="5"
|
|
365
|
-
page-size="10"
|
|
366
|
-
></ui-pagination>
|
|
367
|
-
|
|
368
|
-
<script>
|
|
369
|
-
const pagination = document.querySelector('ui-pagination');
|
|
370
|
-
|
|
371
|
-
pagination.addEventListener('page-change', (e) => {
|
|
372
|
-
console.log('Page:', e.detail.page);
|
|
373
|
-
console.log('Total Pages:', e.detail.totalPages);
|
|
374
|
-
// Load new data...
|
|
375
|
-
});
|
|
376
|
-
</script>
|
|
319
|
+
customElements.define('app-nav', NavComponent);
|
|
377
320
|
```
|
|
378
321
|
|
|
379
|
-
**
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
322
|
+
**src/pages/home.ts**
|
|
323
|
+
```typescript
|
|
324
|
+
import { BaseComponent } from '@diniz/webcomponents';
|
|
325
|
+
import '../components/nav';
|
|
383
326
|
|
|
384
|
-
|
|
385
|
-
|
|
327
|
+
class HomePage extends BaseComponent {
|
|
328
|
+
connectedCallback() {
|
|
329
|
+
super.connectedCallback();
|
|
330
|
+
this.render();
|
|
331
|
+
}
|
|
386
332
|
|
|
387
|
-
|
|
388
|
-
|
|
333
|
+
render() {
|
|
334
|
+
this.shadowRoot!.innerHTML = `
|
|
335
|
+
<app-nav></app-nav>
|
|
336
|
+
<h1>Home Page</h1>
|
|
337
|
+
<p>Welcome to my web components app!</p>
|
|
338
|
+
<ui-button variant="primary">
|
|
339
|
+
<a href="/counter" data-link style="color: inherit; text-decoration: none;">
|
|
340
|
+
Try Counter
|
|
341
|
+
</a>
|
|
342
|
+
</ui-button>
|
|
343
|
+
`;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
389
346
|
|
|
390
|
-
|
|
347
|
+
customElements.define('home-page', HomePage);
|
|
348
|
+
```
|
|
391
349
|
|
|
392
|
-
|
|
350
|
+
**src/pages/about.ts**
|
|
351
|
+
```typescript
|
|
352
|
+
import { BaseComponent } from '@diniz/webcomponents';
|
|
353
|
+
import '../components/nav';
|
|
393
354
|
|
|
394
|
-
|
|
355
|
+
class AboutPage extends BaseComponent {
|
|
356
|
+
connectedCallback() {
|
|
357
|
+
super.connectedCallback();
|
|
358
|
+
this.render();
|
|
359
|
+
}
|
|
395
360
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
- Real-time validation feedback
|
|
405
|
-
- Error message display
|
|
406
|
-
- Touched state tracking
|
|
407
|
-
- Disabled state support
|
|
361
|
+
render() {
|
|
362
|
+
this.shadowRoot!.innerHTML = `
|
|
363
|
+
<app-nav></app-nav>
|
|
364
|
+
<h1>About</h1>
|
|
365
|
+
<p>This is a Vite app using web components with routing.</p>
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
408
369
|
|
|
409
|
-
|
|
410
|
-
```html
|
|
411
|
-
<ui-input
|
|
412
|
-
type="email"
|
|
413
|
-
label="Email"
|
|
414
|
-
placeholder="you@example.com"
|
|
415
|
-
required
|
|
416
|
-
validate="emailDomain:company.com"
|
|
417
|
-
></ui-input>
|
|
418
|
-
|
|
419
|
-
<ui-input
|
|
420
|
-
type="password"
|
|
421
|
-
label="Password"
|
|
422
|
-
minlength="8"
|
|
423
|
-
required
|
|
424
|
-
></ui-input>
|
|
370
|
+
customElements.define('about-page', AboutPage);
|
|
425
371
|
```
|
|
426
372
|
|
|
427
|
-
|
|
428
|
-
- `type` - Input type
|
|
429
|
-
- `label` - Label text
|
|
430
|
-
- `placeholder` - Placeholder text
|
|
431
|
-
- `required` - Required field
|
|
432
|
-
- `pattern` - Regex pattern
|
|
433
|
-
- `minlength` / `maxlength` - Length constraints
|
|
434
|
-
- `min` / `max` - Number constraints
|
|
435
|
-
- `error-message` - Custom error message
|
|
436
|
-
- `disabled` - Disable input
|
|
437
|
-
- `name` - Form field name
|
|
438
|
-
- `validate` - Validation rule (e.g., `emailDomain:company.com`)
|
|
439
|
-
|
|
440
|
-
**State:**
|
|
441
|
-
- `value` - Current input value
|
|
442
|
-
- `valid` - Validation state
|
|
443
|
-
- `touched` - Whether field has been interacted with
|
|
444
|
-
- `error` - Current error message
|
|
445
|
-
|
|
446
|
-
---
|
|
447
|
-
|
|
448
|
-
### 🪟 Modal (`ui-modal`)
|
|
449
|
-
|
|
450
|
-
Responsive modal dialog with customizable sizes and behaviors.
|
|
451
|
-
|
|
452
|
-
**Features:**
|
|
453
|
-
- 5 size options: `sm`, `md`, `lg`, `xl`, `full`
|
|
454
|
-
- Auto-close on Escape key (configurable)
|
|
455
|
-
- Auto-close on backdrop click (configurable)
|
|
456
|
-
- Smooth animations (fade in, slide up)
|
|
457
|
-
- Header, body, and footer slots
|
|
458
|
-
- Programmatic open/close API
|
|
459
|
-
- Custom events
|
|
460
|
-
- Body scroll lock when open
|
|
461
|
-
|
|
462
|
-
**Usage:**
|
|
463
|
-
```html
|
|
464
|
-
<ui-button id="openModal">Open Modal</ui-button>
|
|
465
|
-
|
|
466
|
-
<ui-modal id="myModal" title="Welcome!" size="md">
|
|
467
|
-
<p>This is the modal content.</p>
|
|
468
|
-
<p>You can include any HTML here.</p>
|
|
469
|
-
|
|
470
|
-
<div slot="footer">
|
|
471
|
-
<ui-button id="closeBtn" variant="secondary">Cancel</ui-button>
|
|
472
|
-
<ui-button id="confirmBtn" variant="primary">Confirm</ui-button>
|
|
473
|
-
</div>
|
|
474
|
-
</ui-modal>
|
|
373
|
+
#### 3. Initialize the router in main.ts
|
|
475
374
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
closeBtn.addEventListener('click', () => modal.close());
|
|
483
|
-
|
|
484
|
-
modal.addEventListener('modal-close', () => {
|
|
485
|
-
console.log('Modal closed');
|
|
486
|
-
});
|
|
487
|
-
</script>
|
|
375
|
+
**src/main.ts**
|
|
376
|
+
```typescript
|
|
377
|
+
import '@diniz/webcomponents';
|
|
378
|
+
import '@diniz/webcomponents/dist/style.css';
|
|
379
|
+
import './style.css';
|
|
380
|
+
import './router'; // This loads the routes and initializes routing
|
|
488
381
|
```
|
|
489
382
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
- `no-close-on-backdrop` - Disable closing on backdrop click
|
|
496
|
-
|
|
497
|
-
**Methods:**
|
|
498
|
-
- `open()` - Open the modal
|
|
499
|
-
- `close()` - Close the modal
|
|
383
|
+
The moment you import `./router`, the routing system:
|
|
384
|
+
1. ✅ Registers all event listeners for navigation
|
|
385
|
+
2. ✅ Automatically loads the initial route based on the current URL
|
|
386
|
+
3. ✅ Starts handling clicks on `[data-link]` elements
|
|
387
|
+
4. ✅ Enables browser back/forward button support
|
|
500
388
|
|
|
501
|
-
|
|
502
|
-
-
|
|
503
|
-
-
|
|
389
|
+
That's it! Your app now has client-side routing with:
|
|
390
|
+
- ✅ Lazy-loaded pages
|
|
391
|
+
- ✅ Browser back/forward navigation
|
|
392
|
+
- ✅ Declarative routing with `data-link` attribute
|
|
393
|
+
- ✅ Optional route guards for protected pages
|
|
394
|
+
- ✅ Reusable navigation component
|
|
504
395
|
|
|
505
|
-
|
|
396
|
+
## Components
|
|
506
397
|
|
|
507
|
-
|
|
398
|
+
- **ui-button** - Button with variants, sizes, icons
|
|
399
|
+
- **ui-input** - Input with validation
|
|
400
|
+
- **ui-table** - Data table with actions
|
|
401
|
+
- **ui-date-picker** - Date picker
|
|
402
|
+
- **ui-pagination** - Pagination control
|
|
403
|
+
- **ui-select** - Dropdown selection
|
|
404
|
+
- **ui-checkbox** - Checkbox input
|
|
405
|
+
- **ui-modal** - Modal dialog
|
|
406
|
+
- **ui-card** - Card container
|
|
407
|
+
- **ui-tabs** - Tab navigation
|
|
408
|
+
- **ui-stepper** - Step indicator
|
|
409
|
+
- **ui-toast** - Toast notifications
|
|
410
|
+
- **ui-upload** - File upload
|
|
411
|
+
- **ui-layout** - Application layout
|
|
412
|
+
|
|
413
|
+
For detailed documentation on each component, see the demo implementations in `src/features/`.
|
|
508
414
|
|
|
509
|
-
|
|
415
|
+
## Core Features
|
|
510
416
|
|
|
511
|
-
|
|
512
|
-
- JSON-based options configuration
|
|
513
|
-
- Searchable dropdown (optional)
|
|
514
|
-
- Keyboard navigation
|
|
515
|
-
- Disabled options support
|
|
516
|
-
- Custom placeholder text
|
|
517
|
-
- Change events with full option details
|
|
518
|
-
- Click-outside to close
|
|
519
|
-
- Smooth animations
|
|
520
|
-
- Theme-aware styling
|
|
417
|
+
### Signals & Reactivity
|
|
521
418
|
|
|
522
|
-
|
|
523
|
-
```html
|
|
524
|
-
<ui-select
|
|
525
|
-
id="mySelect"
|
|
526
|
-
label="Choose a Country"
|
|
527
|
-
placeholder="Select country..."
|
|
528
|
-
searchable
|
|
529
|
-
></ui-select>
|
|
530
|
-
|
|
531
|
-
<script>
|
|
532
|
-
const select = document.getElementById('mySelect');
|
|
533
|
-
|
|
534
|
-
// Set options
|
|
535
|
-
const options = [
|
|
536
|
-
{ value: 'us', label: 'United States' },
|
|
537
|
-
{ value: 'uk', label: 'United Kingdom' },
|
|
538
|
-
{ value: 'ca', label: 'Canada' },
|
|
539
|
-
{ value: 'au', label: 'Australia', disabled: true }
|
|
540
|
-
];
|
|
541
|
-
|
|
542
|
-
select.setAttribute('options', JSON.stringify(options));
|
|
543
|
-
|
|
544
|
-
// Set initial value
|
|
545
|
-
select.setAttribute('value', 'us');
|
|
546
|
-
|
|
547
|
-
// Listen for changes
|
|
548
|
-
select.addEventListener('select-change', (e) => {
|
|
549
|
-
console.log('Value:', e.detail.value);
|
|
550
|
-
console.log('Option:', e.detail.option);
|
|
551
|
-
});
|
|
552
|
-
</script>
|
|
553
|
-
```
|
|
419
|
+
Create reactive, auto-updating UI with signals. Changes automatically trigger re-renders:
|
|
554
420
|
|
|
555
|
-
**Attributes:**
|
|
556
|
-
- `label` - Label text above select
|
|
557
|
-
- `placeholder` - Placeholder when no selection
|
|
558
|
-
- `options` - JSON string of options array
|
|
559
|
-
- `value` - Currently selected value
|
|
560
|
-
- `disabled` - Disable the select
|
|
561
|
-
- `searchable` - Enable search functionality
|
|
562
|
-
|
|
563
|
-
**Option Format:**
|
|
564
421
|
```typescript
|
|
565
|
-
{
|
|
566
|
-
value: string; // The option value
|
|
567
|
-
label: string; // Display text
|
|
568
|
-
disabled?: boolean; // Optional: disable option
|
|
569
|
-
}
|
|
570
|
-
```
|
|
422
|
+
import { BaseComponent } from '@diniz/webcomponents';
|
|
571
423
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
424
|
+
class CounterComponent extends BaseComponent {
|
|
425
|
+
// Create a reactive signal with initial value 0
|
|
426
|
+
private count = this.useSignal(0);
|
|
427
|
+
|
|
428
|
+
connectedCallback() {
|
|
429
|
+
super.connectedCallback();
|
|
430
|
+
this.render();
|
|
431
|
+
}
|
|
576
432
|
|
|
577
|
-
|
|
433
|
+
private increment() {
|
|
434
|
+
// Update signal value - automatically triggers re-render
|
|
435
|
+
this.count.set(this.count.get() + 1);
|
|
436
|
+
}
|
|
578
437
|
|
|
579
|
-
|
|
438
|
+
private reset() {
|
|
439
|
+
this.count.set(0);
|
|
440
|
+
}
|
|
580
441
|
|
|
581
|
-
|
|
442
|
+
render() {
|
|
443
|
+
const currentCount = this.count.get();
|
|
444
|
+
|
|
445
|
+
this.shadowRoot!.innerHTML = `
|
|
446
|
+
<style>
|
|
447
|
+
:host {
|
|
448
|
+
display: flex;
|
|
449
|
+
flex-direction: column;
|
|
450
|
+
gap: 1rem;
|
|
451
|
+
padding: 2rem;
|
|
452
|
+
}
|
|
453
|
+
.count-display {
|
|
454
|
+
font-size: 2rem;
|
|
455
|
+
font-weight: bold;
|
|
456
|
+
color: var(--color-primary, #24ec71);
|
|
457
|
+
}
|
|
458
|
+
</style>
|
|
459
|
+
|
|
460
|
+
<div class="count-display">Count: ${currentCount}</div>
|
|
461
|
+
<button id="increment">Increment</button>
|
|
462
|
+
<button id="reset">Reset</button>
|
|
463
|
+
`;
|
|
464
|
+
|
|
465
|
+
// Connect event listeners to update signals
|
|
466
|
+
this.shadowRoot!.getElementById('increment')?.addEventListener('click',
|
|
467
|
+
() => this.increment()
|
|
468
|
+
);
|
|
469
|
+
this.shadowRoot!.getElementById('reset')?.addEventListener('click',
|
|
470
|
+
() => this.reset()
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
582
474
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
- Checked/unchecked states
|
|
586
|
-
- Indeterminate state (useful for "select all")
|
|
587
|
-
- Disabled state
|
|
588
|
-
- Label support (attribute or slot)
|
|
589
|
-
- Programmatic API
|
|
590
|
-
- Custom events
|
|
591
|
-
- Smooth animations and transitions
|
|
592
|
-
- Theme-aware styling
|
|
475
|
+
customElements.define('counter-app', CounterComponent);
|
|
476
|
+
```
|
|
593
477
|
|
|
594
|
-
**Usage:**
|
|
478
|
+
**Usage in HTML:**
|
|
595
479
|
```html
|
|
596
|
-
|
|
597
|
-
<ui-checkbox label="Accept terms"></ui-checkbox>
|
|
598
|
-
<ui-checkbox label="Subscribe" checked></ui-checkbox>
|
|
599
|
-
<ui-checkbox label="Disabled" disabled></ui-checkbox>
|
|
600
|
-
|
|
601
|
-
<!-- With sizes -->
|
|
602
|
-
<ui-checkbox label="Small" size="sm"></ui-checkbox>
|
|
603
|
-
<ui-checkbox label="Medium" size="md"></ui-checkbox>
|
|
604
|
-
<ui-checkbox label="Large" size="lg"></ui-checkbox>
|
|
605
|
-
|
|
606
|
-
<!-- Programmatic usage -->
|
|
607
|
-
<ui-checkbox id="myCheckbox" label="Select All"></ui-checkbox>
|
|
608
|
-
|
|
609
|
-
<script>
|
|
610
|
-
const checkbox = document.getElementById('myCheckbox');
|
|
611
|
-
|
|
612
|
-
// Listen for changes
|
|
613
|
-
checkbox.addEventListener('checkbox-change', (e) => {
|
|
614
|
-
console.log('Checked:', e.detail.checked);
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
// Set states programmatically
|
|
618
|
-
checkbox.setChecked(true);
|
|
619
|
-
checkbox.setIndeterminate(true);
|
|
620
|
-
</script>
|
|
480
|
+
<counter-app></counter-app>
|
|
621
481
|
```
|
|
622
482
|
|
|
623
|
-
**
|
|
624
|
-
- `
|
|
625
|
-
- `
|
|
626
|
-
- `
|
|
627
|
-
- `
|
|
628
|
-
-
|
|
483
|
+
**Key features:**
|
|
484
|
+
- `this.useSignal(value)` - Create a reactive signal
|
|
485
|
+
- `signal.get()` - Read the current value
|
|
486
|
+
- `signal.set(newValue)` - Update value and trigger re-render
|
|
487
|
+
- `this.setState({ ... })` - Update multiple values at once
|
|
488
|
+
- Changes are isolated within component's shadow DOM
|
|
629
489
|
|
|
630
|
-
|
|
631
|
-
- `setChecked(checked: boolean)` - Set checked state
|
|
632
|
-
- `setIndeterminate(indeterminate: boolean)` - Set indeterminate state
|
|
490
|
+
### Signals with Separated HTML & TypeScript Files
|
|
633
491
|
|
|
634
|
-
|
|
635
|
-
- `checkbox-change` - Fired when state changes
|
|
636
|
-
- `detail.checked` - New checked state
|
|
492
|
+
When using separate HTML template files, you can achieve automatic reactivity so the HTML updates whenever a signal changes:
|
|
637
493
|
|
|
638
|
-
|
|
494
|
+
**counter.html**
|
|
495
|
+
```html
|
|
496
|
+
<style>
|
|
497
|
+
:host {
|
|
498
|
+
display: flex;
|
|
499
|
+
flex-direction: column;
|
|
500
|
+
gap: 1rem;
|
|
501
|
+
padding: 2rem;
|
|
502
|
+
}
|
|
503
|
+
.count-display {
|
|
504
|
+
font-size: 2rem;
|
|
505
|
+
font-weight: bold;
|
|
506
|
+
color: var(--color-primary, #24ec71);
|
|
507
|
+
}
|
|
508
|
+
</style>
|
|
639
509
|
|
|
640
|
-
|
|
510
|
+
<div class="count-display">
|
|
511
|
+
Count: <span id="countValue">0</span>
|
|
512
|
+
</div>
|
|
513
|
+
<button id="incrementBtn">Increment</button>
|
|
514
|
+
<button id="resetBtn">Reset</button>
|
|
515
|
+
```
|
|
641
516
|
|
|
642
|
-
|
|
517
|
+
**counter.ts - Automatic Reactivity Approach**
|
|
518
|
+
```typescript
|
|
519
|
+
import { BaseComponent } from '@diniz/webcomponents';
|
|
520
|
+
import template from './counter.html?raw';
|
|
643
521
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
522
|
+
class CounterComponent extends BaseComponent {
|
|
523
|
+
private count = this.useSignal(0);
|
|
524
|
+
|
|
525
|
+
connectedCallback() {
|
|
526
|
+
super.connectedCallback();
|
|
527
|
+
this.render();
|
|
528
|
+
this.setupEventListeners();
|
|
529
|
+
|
|
530
|
+
// Subscribe to signal changes - re-render when count changes
|
|
531
|
+
this.watchSignal(this.count, () => {
|
|
532
|
+
this.updateCountDisplay();
|
|
533
|
+
});
|
|
534
|
+
}
|
|
648
535
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
```
|
|
536
|
+
private increment() {
|
|
537
|
+
this.count.set(this.count.get() + 1);
|
|
538
|
+
}
|
|
653
539
|
|
|
654
|
-
|
|
540
|
+
private reset() {
|
|
541
|
+
this.count.set(0);
|
|
542
|
+
}
|
|
655
543
|
|
|
656
|
-
|
|
544
|
+
// Single method that updates the display - called whenever signal changes
|
|
545
|
+
private updateCountDisplay() {
|
|
546
|
+
const countValue = this.shadowRoot?.getElementById('countValue');
|
|
547
|
+
if (countValue) {
|
|
548
|
+
countValue.textContent = String(this.count.get());
|
|
549
|
+
}
|
|
550
|
+
}
|
|
657
551
|
|
|
658
|
-
|
|
552
|
+
private setupEventListeners() {
|
|
553
|
+
this.shadowRoot?.getElementById('incrementBtn')?.addEventListener('click',
|
|
554
|
+
() => this.increment()
|
|
555
|
+
);
|
|
556
|
+
this.shadowRoot?.getElementById('resetBtn')?.addEventListener('click',
|
|
557
|
+
() => this.reset()
|
|
558
|
+
);
|
|
559
|
+
}
|
|
659
560
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
561
|
+
render() {
|
|
562
|
+
this.shadowRoot!.innerHTML = template;
|
|
563
|
+
this.updateCountDisplay(); // Initial display
|
|
564
|
+
}
|
|
565
|
+
}
|
|
665
566
|
|
|
666
|
-
|
|
667
|
-
```html
|
|
668
|
-
<app-layout>
|
|
669
|
-
<your-page-component></your-page-component>
|
|
670
|
-
</app-layout>
|
|
567
|
+
customElements.define('counter-app', CounterComponent);
|
|
671
568
|
```
|
|
672
569
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
## Core Features
|
|
676
|
-
|
|
677
|
-
### Base Component
|
|
570
|
+
**Alternative: Full Re-render on Signal Change**
|
|
678
571
|
|
|
679
|
-
|
|
572
|
+
For simpler components, you can also re-render the entire template when any signal changes:
|
|
680
573
|
|
|
681
|
-
**Signal-based Reactivity:**
|
|
682
574
|
```typescript
|
|
683
|
-
class
|
|
575
|
+
class CounterComponent extends BaseComponent {
|
|
684
576
|
private count = this.useSignal(0);
|
|
685
577
|
|
|
686
578
|
connectedCallback() {
|
|
687
579
|
super.connectedCallback();
|
|
688
|
-
|
|
689
|
-
this.
|
|
580
|
+
this.render();
|
|
581
|
+
this.setupEventListeners();
|
|
582
|
+
// Re-render entire component when signal changes
|
|
583
|
+
this.watchSignal(this.count, () => this.render());
|
|
690
584
|
}
|
|
691
|
-
}
|
|
692
|
-
```
|
|
693
585
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
constructor() {
|
|
698
|
-
super();
|
|
699
|
-
this.state = { user: '' };
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
updateUser() {
|
|
703
|
-
this.setState({ user: 'Alice' }); // Triggers re-render
|
|
586
|
+
private increment() {
|
|
587
|
+
this.count.set(this.count.get() + 1);
|
|
588
|
+
// No need to manually update - render() is called automatically
|
|
704
589
|
}
|
|
705
|
-
}
|
|
706
|
-
```
|
|
707
590
|
|
|
708
|
-
|
|
591
|
+
private reset() {
|
|
592
|
+
this.count.set(0);
|
|
593
|
+
}
|
|
709
594
|
|
|
710
|
-
|
|
595
|
+
private setupEventListeners() {
|
|
596
|
+
// Re-attach listeners after each render
|
|
597
|
+
this.shadowRoot?.getElementById('incrementBtn')?.addEventListener('click',
|
|
598
|
+
() => this.increment()
|
|
599
|
+
);
|
|
600
|
+
this.shadowRoot?.getElementById('resetBtn')?.addEventListener('click',
|
|
601
|
+
() => this.reset()
|
|
602
|
+
);
|
|
603
|
+
}
|
|
711
604
|
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
605
|
+
render() {
|
|
606
|
+
const currentCount = this.count.get();
|
|
607
|
+
|
|
608
|
+
this.shadowRoot!.innerHTML = `
|
|
609
|
+
<style>
|
|
610
|
+
:host {
|
|
611
|
+
display: flex;
|
|
612
|
+
flex-direction: column;
|
|
613
|
+
gap: 1rem;
|
|
614
|
+
padding: 2rem;
|
|
615
|
+
}
|
|
616
|
+
.count-display {
|
|
617
|
+
font-size: 2rem;
|
|
618
|
+
font-weight: bold;
|
|
619
|
+
color: var(--color-primary, #24ec71);
|
|
620
|
+
}
|
|
621
|
+
</style>
|
|
622
|
+
|
|
623
|
+
<div class="count-display">Count: ${currentCount}</div>
|
|
624
|
+
<button id="incrementBtn">Increment</button>
|
|
625
|
+
<button id="resetBtn">Reset</button>
|
|
626
|
+
`;
|
|
627
|
+
|
|
628
|
+
this.setupEventListeners();
|
|
719
629
|
}
|
|
720
|
-
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
customElements.define('counter-app', CounterComponent);
|
|
721
633
|
```
|
|
722
634
|
|
|
723
|
-
|
|
635
|
+
**Key improvements:**
|
|
636
|
+
- Use `this.watchSignal(signal, callback)` to subscribe to signal changes
|
|
637
|
+
- When signal updates, callback triggers automatically - no manual DOM updates needed
|
|
638
|
+
- Choose between:
|
|
639
|
+
- **Selective updates** - Only update specific DOM elements (better performance)
|
|
640
|
+
- **Full re-render** - Re-render entire template (simpler logic, less efficient)
|
|
641
|
+
- The HTML automatically stays in sync with signal values
|
|
724
642
|
|
|
725
|
-
|
|
643
|
+
### Enhanced: useSignalHtml for Direct DOM Binding
|
|
726
644
|
|
|
727
|
-
|
|
728
|
-
import { store } from './core/store';
|
|
645
|
+
For even cleaner code, use `useSignalHtml()` to create a signal that automatically updates a specific HTML element:
|
|
729
646
|
|
|
730
|
-
|
|
731
|
-
|
|
647
|
+
**counter.html**
|
|
648
|
+
```html
|
|
649
|
+
<style>
|
|
650
|
+
:host {
|
|
651
|
+
display: flex;
|
|
652
|
+
flex-direction: column;
|
|
653
|
+
gap: 1rem;
|
|
654
|
+
padding: 2rem;
|
|
655
|
+
}
|
|
656
|
+
.count-display {
|
|
657
|
+
font-size: 2rem;
|
|
658
|
+
font-weight: bold;
|
|
659
|
+
color: var(--color-primary, #24ec71);
|
|
660
|
+
}
|
|
661
|
+
</style>
|
|
732
662
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
663
|
+
<div class="count-display">
|
|
664
|
+
Count: <span id="countValue">0</span>
|
|
665
|
+
</div>
|
|
666
|
+
<button id="incrementBtn">Increment</button>
|
|
667
|
+
<button id="resetBtn">Reset</button>
|
|
736
668
|
```
|
|
737
669
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
Lightweight HTTP client with interceptor support for API requests:
|
|
741
|
-
|
|
670
|
+
**counter.ts - Simplified with useSignalHtml**
|
|
742
671
|
```typescript
|
|
743
|
-
import {
|
|
672
|
+
import { BaseComponent } from '@diniz/webcomponents';
|
|
673
|
+
import template from './counter.html?raw';
|
|
744
674
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
675
|
+
class CounterComponent extends BaseComponent {
|
|
676
|
+
// Create signal bound to HTML element with ID 'countValue'
|
|
677
|
+
// Automatically updates the element's textContent when signal changes
|
|
678
|
+
private count = this.useSignalHtml('countValue', 0);
|
|
679
|
+
|
|
680
|
+
connectedCallback() {
|
|
681
|
+
super.connectedCallback();
|
|
682
|
+
this.render();
|
|
683
|
+
this.setupEventListeners();
|
|
684
|
+
}
|
|
750
685
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
await http.delete(`/users/${id}`);
|
|
756
|
-
```
|
|
686
|
+
private increment() {
|
|
687
|
+
// Just update the signal - HTML updates automatically
|
|
688
|
+
this.count.set(this.count.get() + 1);
|
|
689
|
+
}
|
|
757
690
|
|
|
758
|
-
|
|
691
|
+
private reset() {
|
|
692
|
+
// Just update the signal - HTML updates automatically
|
|
693
|
+
this.count.set(0);
|
|
694
|
+
}
|
|
759
695
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
return config;
|
|
769
|
-
});
|
|
696
|
+
private setupEventListeners() {
|
|
697
|
+
this.shadowRoot?.getElementById('incrementBtn')?.addEventListener('click',
|
|
698
|
+
() => this.increment()
|
|
699
|
+
);
|
|
700
|
+
this.shadowRoot?.getElementById('resetBtn')?.addEventListener('click',
|
|
701
|
+
() => this.reset()
|
|
702
|
+
);
|
|
703
|
+
}
|
|
770
704
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
(config) => config,
|
|
774
|
-
(error) => {
|
|
775
|
-
console.error('Request failed:', error);
|
|
776
|
-
throw error;
|
|
705
|
+
render() {
|
|
706
|
+
this.shadowRoot!.innerHTML = template;
|
|
777
707
|
}
|
|
778
|
-
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
customElements.define('counter-app', CounterComponent);
|
|
779
711
|
```
|
|
780
712
|
|
|
781
|
-
**
|
|
713
|
+
**Benefits of `useSignalHtml()`:**
|
|
714
|
+
- No need for `watchSignal()` subscription
|
|
715
|
+
- No need for manual `updateDisplay()` functions
|
|
716
|
+
- Automatically syncs signal value to element's `textContent`
|
|
717
|
+
- One line instead of multiple lines of setup
|
|
718
|
+
- Perfect for data binding in separated HTML/TS architecture
|
|
782
719
|
|
|
720
|
+
**Signature:**
|
|
783
721
|
```typescript
|
|
784
|
-
|
|
785
|
-
http.interceptors.response.use((response) => {
|
|
786
|
-
// Unwrap API response if it's nested
|
|
787
|
-
if (response.data?.result) {
|
|
788
|
-
response.data = response.data.result;
|
|
789
|
-
}
|
|
790
|
-
return response;
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
// Handle errors globally
|
|
794
|
-
http.interceptors.response.use(
|
|
795
|
-
(response) => response,
|
|
796
|
-
(error) => {
|
|
797
|
-
if (error.response?.status === 401) {
|
|
798
|
-
// Handle unauthorized
|
|
799
|
-
localStorage.removeItem('auth_token');
|
|
800
|
-
window.location.href = '/login';
|
|
801
|
-
}
|
|
802
|
-
throw error;
|
|
803
|
-
}
|
|
804
|
-
);
|
|
722
|
+
useSignalHtml<T>(elementId: string, initialValue: T): Signal<T>
|
|
805
723
|
```
|
|
806
724
|
|
|
807
|
-
|
|
725
|
+
Returns a signal that automatically updates the specified HTML element whenever the value changes.
|
|
808
726
|
|
|
809
|
-
|
|
810
|
-
- `post<T>(url, data?, config?)` - POST request
|
|
811
|
-
- `put<T>(url, data?, config?)` - PUT request
|
|
812
|
-
- `patch<T>(url, data?, config?)` - PATCH request
|
|
813
|
-
- `delete<T>(url, config?)` - DELETE request
|
|
814
|
-
- `head<T>(url, config?)` - HEAD request
|
|
727
|
+
This pattern is used throughout the demo components in `src/features/`.
|
|
815
728
|
|
|
816
|
-
|
|
729
|
+
### HTTP Client
|
|
817
730
|
|
|
818
731
|
```typescript
|
|
819
|
-
|
|
820
|
-
method?: string;
|
|
821
|
-
headers?: Record<string, string>;
|
|
822
|
-
body?: string | FormData | null;
|
|
823
|
-
timeout?: number; // Default: 30000ms
|
|
824
|
-
}
|
|
825
|
-
```
|
|
732
|
+
import { http } from '@diniz/webcomponents';
|
|
826
733
|
|
|
827
|
-
|
|
734
|
+
const users = await http.get<User[]>('/api/users');
|
|
735
|
+
const newUser = await http.post<User>('/api/users', data);
|
|
736
|
+
```
|
|
828
737
|
|
|
829
|
-
|
|
830
|
-
- ✅ Automatic JSON serialization/deserialization
|
|
831
|
-
- ✅ Timeout support (default 30s)
|
|
832
|
-
- ✅ Global headers and base URL configuration
|
|
833
|
-
- ✅ FormData support for file uploads
|
|
834
|
-
- ✅ TypeScript generics for type-safe responses
|
|
835
|
-
- ✅ Automatic error messages with status codes
|
|
738
|
+
### Router & Store
|
|
836
739
|
|
|
837
|
-
|
|
740
|
+
Built-in routing and global state management utilities.
|
|
838
741
|
|
|
839
742
|
## Theming
|
|
840
743
|
|
|
841
|
-
|
|
744
|
+
Customize colors and spacing using CSS custom properties:
|
|
842
745
|
|
|
843
746
|
```css
|
|
844
747
|
:root {
|
|
845
748
|
--color-primary: #24ec71;
|
|
846
|
-
--color-
|
|
847
|
-
--color-ink: #0f172a;
|
|
848
|
-
--color-muted: #f1f5f9;
|
|
849
|
-
--color-border: #e2e8f0;
|
|
749
|
+
--color-secondary: #8b5cf6;
|
|
850
750
|
--radius-md: 12px;
|
|
851
|
-
--radius-pill: 999px;
|
|
852
751
|
}
|
|
853
752
|
```
|
|
854
753
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
## Dependencies
|
|
858
|
-
|
|
859
|
-
### Icons
|
|
860
|
-
|
|
861
|
-
This library uses **[Feather Icons](https://feathericons.com/)** for beautiful, minimal SVG icons. Feather provides a consistent set of 286 icons perfect for UI components.
|
|
862
|
-
|
|
863
|
-
```typescript
|
|
864
|
-
import feather from 'feather-icons';
|
|
754
|
+
## Development
|
|
865
755
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
756
|
+
```bash
|
|
757
|
+
npm install
|
|
758
|
+
npm run dev # Start dev server
|
|
759
|
+
npm run build # Build for production
|
|
760
|
+
npm run build:lib # Build library distribution
|
|
869
761
|
```
|
|
870
762
|
|
|
871
|
-
[Browse all available Feather icons →](https://feathericons.com/)
|
|
872
|
-
|
|
873
|
-
---
|
|
874
|
-
|
|
875
|
-
## Bundle Size
|
|
876
|
-
|
|
877
|
-
@diniz/webcomponents is extremely lightweight with zero runtime dependencies:
|
|
878
|
-
|
|
879
|
-
| Package | Size (minified) | Size (gzipped) |
|
|
880
|
-
|---------|-----------------|----------------|
|
|
881
|
-
| **@diniz/webcomponents** | ~45KB | ~12KB |
|
|
882
|
-
| Vue 3 + Router | ~185KB | ~65KB |
|
|
883
|
-
| React 18 + Router | ~245KB | ~85KB |
|
|
884
|
-
| Angular 15 | ~500KB+ | ~150KB+ |
|
|
885
|
-
| Svelte | ~60KB | ~15KB |
|
|
886
|
-
|
|
887
|
-
*Sizes are approximate and vary based on included components and tree-shaking effectiveness*
|
|
888
|
-
|
|
889
|
-
**Why Web Components?**
|
|
890
|
-
- ✅ No framework overhead - use with any framework or vanilla JS
|
|
891
|
-
- ✅ Smaller initial bundle size than traditional frameworks
|
|
892
|
-
- ✅ Progressive enhancement - works without JavaScript
|
|
893
|
-
- ✅ Share components across different projects/frameworks
|
|
894
|
-
- ✅ Built-in browser APIs - no external polyfills needed for modern browsers
|
|
895
|
-
|
|
896
|
-
---
|
|
897
|
-
|
|
898
763
|
## Browser Support
|
|
899
764
|
|
|
900
765
|
- ✅ Chrome/Edge (latest)
|
|
901
766
|
- ✅ Firefox (latest)
|
|
902
767
|
- ✅ Safari (latest)
|
|
903
|
-
- ✅ All modern browsers with Custom Elements support
|
|
904
|
-
|
|
905
|
-
---
|
|
906
|
-
|
|
907
|
-
## Development
|
|
908
|
-
|
|
909
|
-
```bash
|
|
910
|
-
# Install dependencies
|
|
911
|
-
npm install
|
|
912
|
-
|
|
913
|
-
# Start dev server
|
|
914
|
-
npm run dev
|
|
915
|
-
|
|
916
|
-
# Build library
|
|
917
|
-
npm run build:lib
|
|
918
|
-
|
|
919
|
-
# Build production app
|
|
920
|
-
npm run build:prod
|
|
921
|
-
```
|
|
922
|
-
|
|
923
|
-
---
|
|
924
|
-
|
|
925
|
-
## Project Structure
|
|
926
|
-
|
|
927
|
-
```
|
|
928
|
-
src/
|
|
929
|
-
├── core/
|
|
930
|
-
│ ├── base-component.ts # Base class with signals
|
|
931
|
-
│ ├── router.ts # Client-side routing
|
|
932
|
-
│ └── store.ts # Global state management
|
|
933
|
-
├── shared/
|
|
934
|
-
│ └── components/ # Reusable UI components
|
|
935
|
-
│ ├── button.ts
|
|
936
|
-
│ ├── checkbox.ts
|
|
937
|
-
│ ├── date-picker.ts
|
|
938
|
-
│ ├── input.ts
|
|
939
|
-
│ ├── modal.ts
|
|
940
|
-
│ ├── pagination.ts
|
|
941
|
-
│ ├── select.ts
|
|
942
|
-
│ └── table.ts
|
|
943
|
-
├── layouts/
|
|
944
|
-
│ └── app-layout.ts # Application shell
|
|
945
|
-
├── features/ # Page components
|
|
946
|
-
└── styles/
|
|
947
|
-
└── theme.css # Global theme variables
|
|
948
|
-
```
|
|
949
|
-
## Contributing
|
|
950
|
-
|
|
951
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
952
|
-
|
|
953
|
-
---
|
|
954
768
|
|
|
955
769
|
## License
|
|
956
770
|
|
|
957
771
|
MIT © Rodrigo Diniz
|
|
958
772
|
|
|
959
|
-
|