@wsxjs/wsx-base-components 0.0.8 → 0.0.9
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/dist/index.cjs +2 -2
- package/dist/index.js +1882 -935
- package/dist/style.css +1 -0
- package/package.json +4 -4
- package/src/TodoList.css +197 -0
- package/src/TodoList.wsx +264 -0
- package/src/TodoListLight.css +198 -0
- package/src/TodoListLight.wsx +263 -0
- package/src/UserProfile.css +146 -0
- package/src/UserProfile.wsx +247 -0
- package/src/UserProfileLight.css +146 -0
- package/src/UserProfileLight.wsx +256 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
/**
|
|
3
|
+
* TodoListLight Component - LightComponent with @state array support
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* - Using @state decorator with arrays in LightComponent
|
|
7
|
+
* - Array operations: add, remove, update, toggle
|
|
8
|
+
* - Reactive array updates trigger automatic rerender
|
|
9
|
+
* - Light DOM (no Shadow DOM) - styles can be inherited from parent
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LightComponent, state, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
13
|
+
import "./TodoListLight.css";
|
|
14
|
+
|
|
15
|
+
const logger = createLogger("TodoListLight");
|
|
16
|
+
|
|
17
|
+
interface TodoItem {
|
|
18
|
+
id: number;
|
|
19
|
+
text: string;
|
|
20
|
+
completed: boolean;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@autoRegister({ tagName: "todo-list-light" })
|
|
25
|
+
export default class TodoListLight extends LightComponent {
|
|
26
|
+
// @state decorator with array - automatically reactive
|
|
27
|
+
@state private todos: TodoItem[] = [];
|
|
28
|
+
|
|
29
|
+
// Non-reactive input value - prevents rerender on every keystroke
|
|
30
|
+
private _newTodoText = "";
|
|
31
|
+
|
|
32
|
+
// @state decorator with filter state
|
|
33
|
+
@state private filter: "all" | "active" | "completed" = "all";
|
|
34
|
+
|
|
35
|
+
private nextId = 1;
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
logger.info("TodoListLight initialized");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render() {
|
|
43
|
+
const filteredTodos = this.getFilteredTodos();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div class="todo-list-light">
|
|
47
|
+
<div class="todo-header">
|
|
48
|
+
<h2>📝 Todo List (LightComponent)</h2>
|
|
49
|
+
<p class="subtitle">
|
|
50
|
+
{this.todos.length} total, {this.getActiveCount()} active,{" "}
|
|
51
|
+
{this.getCompletedCount()} completed
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="todo-input-section">
|
|
56
|
+
<input
|
|
57
|
+
type="text"
|
|
58
|
+
class="todo-input"
|
|
59
|
+
placeholder="Add a new todo..."
|
|
60
|
+
value={this._newTodoText}
|
|
61
|
+
onInput={this.handleInputChange}
|
|
62
|
+
onKeyDown={this.handleKeyDown}
|
|
63
|
+
/>
|
|
64
|
+
<button
|
|
65
|
+
class="btn btn-primary"
|
|
66
|
+
onClick={this.addTodo}
|
|
67
|
+
disabled={!this._newTodoText.trim()}
|
|
68
|
+
>
|
|
69
|
+
Add
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="todo-filters">
|
|
74
|
+
<button
|
|
75
|
+
class={`filter-btn ${this.filter === "all" ? "active" : ""}`}
|
|
76
|
+
onClick={() => (this.filter = "all")}
|
|
77
|
+
>
|
|
78
|
+
All ({this.todos.length})
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
class={`filter-btn ${this.filter === "active" ? "active" : ""}`}
|
|
82
|
+
onClick={() => (this.filter = "active")}
|
|
83
|
+
>
|
|
84
|
+
Active ({this.getActiveCount()})
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
class={`filter-btn ${this.filter === "completed" ? "active" : ""}`}
|
|
88
|
+
onClick={() => (this.filter = "completed")}
|
|
89
|
+
>
|
|
90
|
+
Completed ({this.getCompletedCount()})
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="todo-list-container">
|
|
95
|
+
{filteredTodos.length === 0 ? (
|
|
96
|
+
<div class="empty-state">
|
|
97
|
+
{this.filter === "all"
|
|
98
|
+
? "No todos yet. Add one above! 🎉"
|
|
99
|
+
: `No ${this.filter} todos.`}
|
|
100
|
+
</div>
|
|
101
|
+
) : (
|
|
102
|
+
<ul class="todo-items">
|
|
103
|
+
{filteredTodos.map((todo) => (
|
|
104
|
+
<li
|
|
105
|
+
key={todo.id}
|
|
106
|
+
class={`todo-item ${todo.completed ? "completed" : ""}`}
|
|
107
|
+
>
|
|
108
|
+
<input
|
|
109
|
+
type="checkbox"
|
|
110
|
+
checked={todo.completed}
|
|
111
|
+
onChange={() => this.toggleTodo(todo.id)}
|
|
112
|
+
class="todo-checkbox"
|
|
113
|
+
/>
|
|
114
|
+
<span class="todo-text">{todo.text}</span>
|
|
115
|
+
<button
|
|
116
|
+
class="btn btn-sm btn-danger"
|
|
117
|
+
onClick={() => this.removeTodo(todo.id)}
|
|
118
|
+
>
|
|
119
|
+
Delete
|
|
120
|
+
</button>
|
|
121
|
+
</li>
|
|
122
|
+
))}
|
|
123
|
+
</ul>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{this.todos.length > 0 && (
|
|
128
|
+
<div class="todo-actions">
|
|
129
|
+
<button class="btn btn-warning" onClick={this.clearCompleted}>
|
|
130
|
+
Clear Completed
|
|
131
|
+
</button>
|
|
132
|
+
<button class="btn btn-danger" onClick={this.clearAll}>
|
|
133
|
+
Clear All
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
<div class="debug-info">
|
|
139
|
+
<details>
|
|
140
|
+
<summary>Debug Info</summary>
|
|
141
|
+
<pre>
|
|
142
|
+
{JSON.stringify(
|
|
143
|
+
{
|
|
144
|
+
todosCount: this.todos.length,
|
|
145
|
+
filter: this.filter,
|
|
146
|
+
newTodoText: this._newTodoText,
|
|
147
|
+
todos: this.todos,
|
|
148
|
+
},
|
|
149
|
+
null,
|
|
150
|
+
2
|
|
151
|
+
)}
|
|
152
|
+
</pre>
|
|
153
|
+
</details>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private handleInputChange = (event: Event) => {
|
|
160
|
+
const input = event.target as HTMLInputElement;
|
|
161
|
+
// Update non-reactive state without triggering rerender
|
|
162
|
+
this._newTodoText = input.value;
|
|
163
|
+
// Update button disabled state directly without rerender
|
|
164
|
+
const button = this.querySelector(".todo-input-section .btn-primary") as HTMLButtonElement;
|
|
165
|
+
if (button) {
|
|
166
|
+
button.disabled = !this._newTodoText.trim();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
private handleKeyDown = (event: KeyboardEvent) => {
|
|
171
|
+
if (event.key === "Enter" && this._newTodoText.trim()) {
|
|
172
|
+
this.addTodo();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
private addTodo = () => {
|
|
177
|
+
// Get input value directly from DOM to avoid state sync issues
|
|
178
|
+
const input = this.querySelector(".todo-input") as HTMLInputElement;
|
|
179
|
+
const text = input?.value.trim() || this._newTodoText.trim();
|
|
180
|
+
|
|
181
|
+
if (!text) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Array mutation - automatically reactive (triggers rerender)
|
|
186
|
+
this.todos = [
|
|
187
|
+
...this.todos,
|
|
188
|
+
{
|
|
189
|
+
id: this.nextId++,
|
|
190
|
+
text,
|
|
191
|
+
completed: false,
|
|
192
|
+
createdAt: Date.now(),
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
// Clear input - update both state and DOM
|
|
197
|
+
this._newTodoText = "";
|
|
198
|
+
if (input) {
|
|
199
|
+
input.value = "";
|
|
200
|
+
// Update button state
|
|
201
|
+
const button = this.querySelector(
|
|
202
|
+
".todo-input-section .btn-primary"
|
|
203
|
+
) as HTMLButtonElement;
|
|
204
|
+
if (button) {
|
|
205
|
+
button.disabled = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
logger.debug("Todo added", { count: this.todos.length });
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
private removeTodo = (id: number) => {
|
|
212
|
+
// Array filter - creates new array, automatically reactive
|
|
213
|
+
this.todos = this.todos.filter((todo) => todo.id !== id);
|
|
214
|
+
logger.debug("Todo removed", { id, remaining: this.todos.length });
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
private toggleTodo = (id: number) => {
|
|
218
|
+
// Array map - creates new array with updated item, automatically reactive
|
|
219
|
+
this.todos = this.todos.map((todo) =>
|
|
220
|
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
221
|
+
);
|
|
222
|
+
logger.debug("Todo toggled", { id });
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
private clearCompleted = () => {
|
|
226
|
+
// Array filter - removes all completed items
|
|
227
|
+
this.todos = this.todos.filter((todo) => !todo.completed);
|
|
228
|
+
logger.debug("Completed todos cleared");
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
private clearAll = () => {
|
|
232
|
+
// Array assignment - clears all items
|
|
233
|
+
this.todos = [];
|
|
234
|
+
logger.debug("All todos cleared");
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
private getFilteredTodos(): TodoItem[] {
|
|
238
|
+
switch (this.filter) {
|
|
239
|
+
case "active":
|
|
240
|
+
return this.todos.filter((todo) => !todo.completed);
|
|
241
|
+
case "completed":
|
|
242
|
+
return this.todos.filter((todo) => todo.completed);
|
|
243
|
+
default:
|
|
244
|
+
return this.todos;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private getActiveCount(): number {
|
|
249
|
+
return this.todos.filter((todo) => !todo.completed).length;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private getCompletedCount(): number {
|
|
253
|
+
return this.todos.filter((todo) => todo.completed).length;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected onConnected(): void {
|
|
257
|
+
logger.info("TodoListLight connected to DOM");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
protected onDisconnected(): void {
|
|
261
|
+
logger.info("TodoListLight disconnected from DOM");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
.user-profile {
|
|
2
|
+
padding: 1.5rem;
|
|
3
|
+
max-width: 800px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.profile-header {
|
|
9
|
+
margin-bottom: 2rem;
|
|
10
|
+
text-align: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.profile-header h2 {
|
|
14
|
+
margin: 0 0 0.5rem 0;
|
|
15
|
+
color: #333;
|
|
16
|
+
font-size: 1.75rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.profile-header .subtitle {
|
|
20
|
+
color: #666;
|
|
21
|
+
font-size: 0.9rem;
|
|
22
|
+
margin: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.profile-content {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: 2rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.profile-section {
|
|
32
|
+
background: #f8f9fa;
|
|
33
|
+
padding: 1.5rem;
|
|
34
|
+
border-radius: 8px;
|
|
35
|
+
border: 1px solid #e0e0e0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.profile-section h3 {
|
|
39
|
+
margin: 0 0 1rem 0;
|
|
40
|
+
color: #333;
|
|
41
|
+
font-size: 1.25rem;
|
|
42
|
+
border-bottom: 2px solid #007bff;
|
|
43
|
+
padding-bottom: 0.5rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.form-group {
|
|
47
|
+
margin-bottom: 1rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.form-group label {
|
|
51
|
+
display: block;
|
|
52
|
+
margin-bottom: 0.5rem;
|
|
53
|
+
color: #555;
|
|
54
|
+
font-weight: 500;
|
|
55
|
+
font-size: 0.9rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.form-group input[type="checkbox"] {
|
|
59
|
+
margin-right: 0.5rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.input-field {
|
|
63
|
+
width: 100%;
|
|
64
|
+
padding: 0.75rem;
|
|
65
|
+
border: 1px solid #ddd;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
font-size: 1rem;
|
|
68
|
+
transition: border-color 0.2s;
|
|
69
|
+
box-sizing: border-box;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.input-field:focus {
|
|
73
|
+
outline: none;
|
|
74
|
+
border-color: #007bff;
|
|
75
|
+
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.profile-actions {
|
|
79
|
+
display: flex;
|
|
80
|
+
gap: 1rem;
|
|
81
|
+
flex-wrap: wrap;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.btn {
|
|
85
|
+
padding: 0.75rem 1.5rem;
|
|
86
|
+
border: none;
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
font-size: 1rem;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
transition: all 0.2s;
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.btn-primary {
|
|
95
|
+
background: #007bff;
|
|
96
|
+
color: white;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.btn-primary:hover {
|
|
100
|
+
background: #0056b3;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.btn-secondary {
|
|
104
|
+
background: #6c757d;
|
|
105
|
+
color: white;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.btn-secondary:hover {
|
|
109
|
+
background: #545b62;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.btn-warning {
|
|
113
|
+
background: #ffc107;
|
|
114
|
+
color: #212529;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.btn-warning:hover {
|
|
118
|
+
background: #e0a800;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.profile-display {
|
|
122
|
+
background: #f8f9fa;
|
|
123
|
+
padding: 1.5rem;
|
|
124
|
+
border-radius: 8px;
|
|
125
|
+
border: 1px solid #e0e0e0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.profile-display h3 {
|
|
129
|
+
margin: 0 0 1rem 0;
|
|
130
|
+
color: #333;
|
|
131
|
+
font-size: 1.25rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.json-display {
|
|
135
|
+
background: #2d2d2d;
|
|
136
|
+
color: #f8f8f2;
|
|
137
|
+
padding: 1rem;
|
|
138
|
+
border-radius: 4px;
|
|
139
|
+
overflow-x: auto;
|
|
140
|
+
font-family: "Courier New", monospace;
|
|
141
|
+
font-size: 0.875rem;
|
|
142
|
+
line-height: 1.5;
|
|
143
|
+
margin: 0;
|
|
144
|
+
white-space: pre-wrap;
|
|
145
|
+
word-wrap: break-word;
|
|
146
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
/**
|
|
3
|
+
* UserProfile Component - WebComponent with @state object support
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* - Using @state decorator with objects in WebComponent
|
|
7
|
+
* - Object property updates trigger automatic rerender
|
|
8
|
+
* - Object replacement (entire object replacement)
|
|
9
|
+
* - Nested object updates
|
|
10
|
+
* - Shadow DOM styling
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { WebComponent, state, autoRegister, createLogger } from "@wsxjs/wsx-core";
|
|
14
|
+
import "./UserProfile.css";
|
|
15
|
+
|
|
16
|
+
const logger = createLogger("UserProfile");
|
|
17
|
+
|
|
18
|
+
interface UserPreferences {
|
|
19
|
+
theme: "light" | "dark";
|
|
20
|
+
language: string;
|
|
21
|
+
notifications: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface UserProfileData {
|
|
25
|
+
name: string;
|
|
26
|
+
email: string;
|
|
27
|
+
age: number;
|
|
28
|
+
preferences: UserPreferences;
|
|
29
|
+
lastLogin: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@autoRegister({ tagName: "user-profile" })
|
|
33
|
+
export default class UserProfile extends WebComponent {
|
|
34
|
+
// @state decorator with object - automatically reactive
|
|
35
|
+
@state private profile: UserProfileData = {
|
|
36
|
+
name: "John Doe",
|
|
37
|
+
email: "john@example.com",
|
|
38
|
+
age: 30,
|
|
39
|
+
preferences: {
|
|
40
|
+
theme: "light",
|
|
41
|
+
language: "en",
|
|
42
|
+
notifications: true,
|
|
43
|
+
},
|
|
44
|
+
lastLogin: Date.now(),
|
|
45
|
+
};
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
logger.info("UserProfile initialized");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
return (
|
|
53
|
+
<div class="user-profile">
|
|
54
|
+
<div class="profile-header">
|
|
55
|
+
<h2>👤 User Profile (WebComponent)</h2>
|
|
56
|
+
<p class="subtitle">Demonstrates @state decorator with objects</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="profile-content">
|
|
60
|
+
<div class="profile-section">
|
|
61
|
+
<h3>Basic Information</h3>
|
|
62
|
+
<div class="form-group">
|
|
63
|
+
<label>Name:</label>
|
|
64
|
+
<input
|
|
65
|
+
type="text"
|
|
66
|
+
value={this.profile.name}
|
|
67
|
+
onInput={this.handleNameChange}
|
|
68
|
+
onBlur={this.handleNameBlur}
|
|
69
|
+
class="input-field"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="form-group">
|
|
73
|
+
<label>Email:</label>
|
|
74
|
+
<input
|
|
75
|
+
type="email"
|
|
76
|
+
value={this.profile.email}
|
|
77
|
+
onInput={this.handleEmailChange}
|
|
78
|
+
onBlur={this.handleEmailBlur}
|
|
79
|
+
class="input-field"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="form-group">
|
|
83
|
+
<label>Age:</label>
|
|
84
|
+
<input
|
|
85
|
+
type="number"
|
|
86
|
+
value={this.profile.age.toString()}
|
|
87
|
+
onInput={this.handleAgeChange}
|
|
88
|
+
onBlur={this.handleAgeBlur}
|
|
89
|
+
class="input-field"
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="profile-section">
|
|
95
|
+
<h3>Preferences</h3>
|
|
96
|
+
<div class="form-group">
|
|
97
|
+
<label>Theme:</label>
|
|
98
|
+
<select
|
|
99
|
+
value={this.profile.preferences.theme}
|
|
100
|
+
onChange={this.handleThemeChange}
|
|
101
|
+
class="input-field"
|
|
102
|
+
>
|
|
103
|
+
<option value="light">Light</option>
|
|
104
|
+
<option value="dark">Dark</option>
|
|
105
|
+
</select>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="form-group">
|
|
108
|
+
<label>Language:</label>
|
|
109
|
+
<select
|
|
110
|
+
value={this.profile.preferences.language}
|
|
111
|
+
onChange={this.handleLanguageChange}
|
|
112
|
+
class="input-field"
|
|
113
|
+
>
|
|
114
|
+
<option value="en">English</option>
|
|
115
|
+
<option value="zh">中文</option>
|
|
116
|
+
<option value="es">Español</option>
|
|
117
|
+
<option value="fr">Français</option>
|
|
118
|
+
</select>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="form-group">
|
|
121
|
+
<label>
|
|
122
|
+
<input
|
|
123
|
+
type="checkbox"
|
|
124
|
+
checked={this.profile.preferences.notifications}
|
|
125
|
+
onChange={this.handleNotificationsChange}
|
|
126
|
+
/>
|
|
127
|
+
Enable Notifications
|
|
128
|
+
</label>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="profile-actions">
|
|
133
|
+
<button class="btn btn-primary" onClick={this.resetProfile}>
|
|
134
|
+
Reset to Default
|
|
135
|
+
</button>
|
|
136
|
+
<button class="btn btn-secondary" onClick={this.updateLastLogin}>
|
|
137
|
+
Update Last Login
|
|
138
|
+
</button>
|
|
139
|
+
<button class="btn btn-warning" onClick={this.replaceEntireProfile}>
|
|
140
|
+
Replace Entire Profile
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="profile-display">
|
|
145
|
+
<h3>Current Profile Data</h3>
|
|
146
|
+
<pre class="json-display">{JSON.stringify(this.profile, null, 2)}</pre>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private handleNameChange = (event: Event) => {
|
|
154
|
+
const input = event.target as HTMLInputElement;
|
|
155
|
+
// Update non-reactive property - no rerender
|
|
156
|
+
this.profile.name = input.value;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
private handleNameBlur = () => {
|
|
160
|
+
// Update reactive profile when input loses focus
|
|
161
|
+
this.profile = { ...this.profile, name: this.profile.name };
|
|
162
|
+
logger.debug("Name updated", { name: this.profile.name });
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
private handleEmailChange = (event: Event) => {
|
|
166
|
+
const input = event.target as HTMLInputElement;
|
|
167
|
+
// Update non-reactive property - no rerender
|
|
168
|
+
this.profile.email = input.value;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
private handleEmailBlur = () => {
|
|
172
|
+
// Update reactive profile when input loses focus
|
|
173
|
+
this.profile = { ...this.profile, email: this.profile.email };
|
|
174
|
+
logger.debug("Email updated", { email: this.profile.email });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
private handleAgeChange = (event: Event) => {
|
|
178
|
+
const input = event.target as HTMLInputElement;
|
|
179
|
+
// Update non-reactive property - no rerender
|
|
180
|
+
this.profile.age = parseInt(input.value, 10) || 0;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
private handleAgeBlur = () => {
|
|
184
|
+
// Update reactive profile when input loses focus
|
|
185
|
+
this.profile = { ...this.profile, age: this.profile.age };
|
|
186
|
+
logger.debug("Age updated", { age: this.profile.age });
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
private handleThemeChange = (event: Event) => {
|
|
190
|
+
const select = event.target as HTMLSelectElement;
|
|
191
|
+
// Direct nested property update - automatically reactive
|
|
192
|
+
this.profile.preferences.theme = select.value as "light" | "dark";
|
|
193
|
+
logger.debug("Theme updated", { theme: select.value });
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
private handleLanguageChange = (event: Event) => {
|
|
197
|
+
const select = event.target as HTMLSelectElement;
|
|
198
|
+
// Direct nested property update - automatically reactive
|
|
199
|
+
this.profile.preferences.language = select.value;
|
|
200
|
+
logger.debug("Language updated", { language: select.value });
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
private handleNotificationsChange = (event: Event) => {
|
|
204
|
+
const checkbox = event.target as HTMLInputElement;
|
|
205
|
+
// Direct nested property update - automatically reactive
|
|
206
|
+
this.profile.preferences.notifications = checkbox.checked;
|
|
207
|
+
logger.debug("Notifications updated", { notifications: checkbox.checked });
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
private resetProfile = () => {
|
|
211
|
+
// Replace entire object - automatically wrapped in reactive
|
|
212
|
+
this.profile = {
|
|
213
|
+
name: "John Doe",
|
|
214
|
+
email: "john@example.com",
|
|
215
|
+
age: 30,
|
|
216
|
+
preferences: {
|
|
217
|
+
theme: "light",
|
|
218
|
+
language: "en",
|
|
219
|
+
notifications: true,
|
|
220
|
+
},
|
|
221
|
+
lastLogin: Date.now(),
|
|
222
|
+
};
|
|
223
|
+
logger.debug("Profile reset to default");
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
private updateLastLogin = () => {
|
|
227
|
+
// Update object property - automatically reactive
|
|
228
|
+
this.profile = { ...this.profile, lastLogin: Date.now() };
|
|
229
|
+
logger.debug("Last login updated", { lastLogin: this.profile.lastLogin });
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
private replaceEntireProfile = () => {
|
|
233
|
+
// Replace entire object - automatically wrapped in reactive
|
|
234
|
+
this.profile = {
|
|
235
|
+
name: "Jane Smith",
|
|
236
|
+
email: "jane@example.com",
|
|
237
|
+
age: 25,
|
|
238
|
+
preferences: {
|
|
239
|
+
theme: "dark",
|
|
240
|
+
language: "zh",
|
|
241
|
+
notifications: false,
|
|
242
|
+
},
|
|
243
|
+
lastLogin: Date.now(),
|
|
244
|
+
};
|
|
245
|
+
logger.debug("Entire profile replaced");
|
|
246
|
+
};
|
|
247
|
+
}
|