native-document 1.0.10 → 1.0.12
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/native-document.dev.js +2671 -0
- package/dist/native-document.min.js +1 -0
- package/docs/anchor.md +212 -0
- package/docs/conditional-rendering.md +628 -0
- package/docs/contributing.md +51 -0
- package/docs/core-concepts.md +513 -0
- package/docs/elements.md +383 -0
- package/docs/getting-started.md +403 -0
- package/docs/lifecycle-events.md +106 -0
- package/docs/memory-management.md +90 -0
- package/docs/observables.md +265 -0
- package/docs/routing.md +817 -0
- package/docs/state-management.md +423 -0
- package/docs/validation.md +193 -0
- package/elements.js +3 -1
- package/index.js +1 -0
- package/package.json +1 -1
- package/readme.md +189 -425
- package/router.js +2 -0
- package/src/data/MemoryManager.js +15 -5
- package/src/data/Observable.js +15 -2
- package/src/data/ObservableChecker.js +3 -0
- package/src/data/ObservableItem.js +4 -0
- package/src/data/Store.js +6 -6
- package/src/elements/anchor.js +1 -1
- package/src/router/Router.js +13 -13
- package/src/router/link.js +8 -5
- package/src/utils/prototypes.js +13 -0
- package/src/utils/validator.js +16 -0
- package/src/wrappers/AttributesWrapper.js +11 -1
- package/src/wrappers/HtmlElementWrapper.js +4 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# Conditional Rendering
|
|
2
|
+
|
|
3
|
+
Conditional rendering in NativeDocument allows you to dynamically show, hide, or switch between different pieces of content based on reactive state. Unlike traditional approaches that require manual DOM manipulation, NativeDocument's conditional rendering utilities automatically update the interface when observable values change.
|
|
4
|
+
|
|
5
|
+
## Understanding Conditional Rendering
|
|
6
|
+
|
|
7
|
+
All conditional rendering functions in NativeDocument work with Observables and automatically manage DOM updates. When the condition changes, content is efficiently added or removed from the DOM without affecting other elements.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { ShowIf, Button, Div, Observable } from 'native-document';
|
|
11
|
+
|
|
12
|
+
const isVisible = Observable(false);
|
|
13
|
+
|
|
14
|
+
// Content automatically appears/disappears based on isVisible
|
|
15
|
+
const conditionalContent = ShowIf(isVisible, 'This content toggles!');
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## ShowIf - Basic Conditional Display
|
|
19
|
+
|
|
20
|
+
The `ShowIf` function renders content only when the Observable condition is truthy. When false, the content is completely removed from the DOM.
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
const user = Observable({ name: 'Alice', isLoggedIn: false });
|
|
24
|
+
|
|
25
|
+
const App = Div([
|
|
26
|
+
Button('Login').nd.on.click(() =>
|
|
27
|
+
user.set({ ...user.val(), isLoggedIn: true })
|
|
28
|
+
),
|
|
29
|
+
Button('Logout').nd.on.click(() =>
|
|
30
|
+
user.set({ ...user.val(), isLoggedIn: false })
|
|
31
|
+
),
|
|
32
|
+
|
|
33
|
+
// Shows only when user is logged in
|
|
34
|
+
ShowIf(user.check(u => u.isLoggedIn), 'Welcome back!')
|
|
35
|
+
]);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Dynamic Content Generation
|
|
39
|
+
|
|
40
|
+
Use functions to create content that reflects current observable values:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
const notifications = Observable.array([]);
|
|
44
|
+
|
|
45
|
+
const notificationBadge = ShowIf(
|
|
46
|
+
notifications.check(list => list.length > 0),
|
|
47
|
+
() => Span({ class: 'badge' }, `${notifications.val().length} new`)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// When notifications change, the badge updates automatically
|
|
51
|
+
notifications.push('New message');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Using Observable Checkers
|
|
55
|
+
|
|
56
|
+
Observable checkers create derived conditions for cleaner code:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
const temperature = Observable(25);
|
|
60
|
+
const isCold = temperature.check(temp => temp < 15);
|
|
61
|
+
const isHot = temperature.check(temp => temp > 30);
|
|
62
|
+
|
|
63
|
+
const WeatherApp = Div([
|
|
64
|
+
Div(['Current temperature: ', temperature, '°C']),
|
|
65
|
+
ShowIf(isCold, Div({ class: 'cold' }, '🧥 It\'s cold! Wear a jacket.')),
|
|
66
|
+
ShowIf(isHot, Div({ class: 'hot' }, '☀️ It\'s hot! Stay hydrated.')),
|
|
67
|
+
Div([
|
|
68
|
+
Button('-').nd.on.click(() => --temperature.$value),
|
|
69
|
+
Button('+').nd.on.click(() => ++temperature.$value),
|
|
70
|
+
])
|
|
71
|
+
]);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## HideIf and HideIfNot - Inverse Conditions
|
|
75
|
+
|
|
76
|
+
These functions provide convenient inverses to `ShowIf`:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
const isLoading = Observable(true);
|
|
80
|
+
const hasData = Observable(false);
|
|
81
|
+
|
|
82
|
+
const DataDisplay = Div([
|
|
83
|
+
// Hide content while loading
|
|
84
|
+
HideIf(isLoading, Div('Data is ready to display')),
|
|
85
|
+
|
|
86
|
+
// Show loading message only while loading
|
|
87
|
+
// Equivalent to ShowIf()
|
|
88
|
+
HideIfNot(isLoading, Div({ class: 'spinner' }, 'Loading...')),
|
|
89
|
+
]);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Understanding the differences:**
|
|
93
|
+
```javascript
|
|
94
|
+
const condition = Observable(true);
|
|
95
|
+
|
|
96
|
+
// These are equivalent:
|
|
97
|
+
ShowIf(condition, content)
|
|
98
|
+
HideIfNot(condition, content)
|
|
99
|
+
|
|
100
|
+
// These are equivalent:
|
|
101
|
+
HideIf(condition, content)
|
|
102
|
+
ShowIf(condition.check(val => !val), content)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Switch - Binary Content Switching
|
|
106
|
+
|
|
107
|
+
`Switch` efficiently toggles between exactly two pieces of content based on a boolean condition:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
const isDarkMode = Observable(false);
|
|
111
|
+
|
|
112
|
+
const ThemeToggle = Div([
|
|
113
|
+
Button('Toggle Theme').nd.on.click(() => isDarkMode.set(!isDarkMode.val())),
|
|
114
|
+
|
|
115
|
+
Switch(isDarkMode,
|
|
116
|
+
Div({ class: 'dark-indicator' }, '🌙 Dark Mode'), // when true
|
|
117
|
+
Div({ class: 'light-indicator' }, '☀️ Light Mode') // when false
|
|
118
|
+
)
|
|
119
|
+
]);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Dynamic Switch Content
|
|
123
|
+
|
|
124
|
+
Functions allow for reactive content that updates with current values:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
const user = Observable({ name: 'Guest', isAuthenticated: false });
|
|
128
|
+
|
|
129
|
+
const UserGreeting = Switch(
|
|
130
|
+
user.check(u => u.isAuthenticated),
|
|
131
|
+
() => Div({ class: 'welcome' }, `Welcome back, ${user.val().name}!`),
|
|
132
|
+
() => Div({ class: 'login-prompt' }, 'Please sign in to continue')
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Match - Multiple Condition Handling
|
|
137
|
+
|
|
138
|
+
`Match` provides switch-case like functionality for handling multiple states:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
const requestStatus = Observable('idle');
|
|
142
|
+
|
|
143
|
+
const StatusDisplay = Match(requestStatus, {
|
|
144
|
+
idle: Div({ class: 'status-idle' }, 'Ready to make request'),
|
|
145
|
+
loading: Div({ class: 'status-loading' }, [
|
|
146
|
+
Span({ class: 'spinner' }),
|
|
147
|
+
' Loading...'
|
|
148
|
+
]),
|
|
149
|
+
success: Div({ class: 'status-success' }, '✅ Request completed'),
|
|
150
|
+
error: Div({ class: 'status-error' }, '❌ Request failed'),
|
|
151
|
+
timeout: Div({ class: 'status-timeout' }, '⏰ Request timed out')
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Dynamic Match Content
|
|
156
|
+
|
|
157
|
+
Functions in Match provide access to current observable values:
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
const gameState = Observable({
|
|
161
|
+
phase: 'menu',
|
|
162
|
+
score: 0,
|
|
163
|
+
level: 1
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const GameDisplay = Match(gameState.check(state => state.phase), {
|
|
167
|
+
menu: () => Div({ class: 'game-menu' }, [
|
|
168
|
+
H1('Welcome to the Game'),
|
|
169
|
+
Button('Start Game').nd.on.click(() =>
|
|
170
|
+
gameState.set({ ...gameState.val(), phase: 'playing' })
|
|
171
|
+
)
|
|
172
|
+
]),
|
|
173
|
+
|
|
174
|
+
playing: () => Div({ class: 'game-ui' }, [
|
|
175
|
+
Div(['Score: ', gameState.check(s => s.score)]),
|
|
176
|
+
Div(['Level: ', gameState.check(s => s.level)]),
|
|
177
|
+
Button('Pause').nd.on.click(() =>
|
|
178
|
+
gameState.set({ ...gameState.val(), phase: 'paused' })
|
|
179
|
+
),
|
|
180
|
+
Button('Game Over').nd.on.click(() =>
|
|
181
|
+
gameState.set({ ...gameState.val(), phase: 'gameOver' })
|
|
182
|
+
)
|
|
183
|
+
]),
|
|
184
|
+
|
|
185
|
+
paused: () => Div({ class: 'game-paused' }, [
|
|
186
|
+
H2('Game Paused'),
|
|
187
|
+
Button('Resume').nd.on.click(() =>
|
|
188
|
+
gameState.set({ ...gameState.val(), phase: 'playing' })
|
|
189
|
+
)
|
|
190
|
+
]),
|
|
191
|
+
|
|
192
|
+
gameOver: () => Div({ class: 'game-over' }, [
|
|
193
|
+
H2('Game Over'),
|
|
194
|
+
Div(['Final Score: ', gameState.check(s => s.score)]),
|
|
195
|
+
Button('Play Again').nd.on.click(() =>
|
|
196
|
+
gameState.set({ phase: 'menu', score: 0, level: 1 })
|
|
197
|
+
)
|
|
198
|
+
])
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Match with Default Cases
|
|
203
|
+
|
|
204
|
+
Handle unexpected values with default cases:
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
const userRole = Observable('guest');
|
|
208
|
+
|
|
209
|
+
const RoleBasedMenu = Match(userRole, {
|
|
210
|
+
admin: AdminMenu(),
|
|
211
|
+
moderator: ModeratorMenu(),
|
|
212
|
+
user: UserMenu(),
|
|
213
|
+
// Default case for unknown roles
|
|
214
|
+
default: GuestMenu()
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## When - Fluent Builder Pattern
|
|
219
|
+
|
|
220
|
+
`When` provides a chainable interface for conditional rendering:
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
const score = Observable(85);
|
|
224
|
+
|
|
225
|
+
const GradeDisplay = When(score.check(s => s >= 90))
|
|
226
|
+
.show(() => Div({ class: 'grade-a' }, `Excellent! Score: ${score.val()}`))
|
|
227
|
+
.otherwise(() => Div({ class: 'grade-b' }, `Score: ${score.val()}`));
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Complex Conditions with When
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
const user = Observable({
|
|
234
|
+
age: 25,
|
|
235
|
+
hasLicense: true,
|
|
236
|
+
hasInsurance: false
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const DrivingEligibility = When(user.check(u =>
|
|
240
|
+
u.age >= 18 && u.hasLicense && u.hasInsurance
|
|
241
|
+
))
|
|
242
|
+
.show(() => Div({ class: 'eligible' }, [
|
|
243
|
+
'✅ You can drive legally',
|
|
244
|
+
Div(`Age: ${user.val().age}, License: Yes, Insurance: Yes`)
|
|
245
|
+
]))
|
|
246
|
+
.otherwise(() => {
|
|
247
|
+
const u = user.val();
|
|
248
|
+
const issues = [];
|
|
249
|
+
if (u.age < 18) issues.push('Must be 18 or older');
|
|
250
|
+
if (!u.hasLicense) issues.push('Need a valid license');
|
|
251
|
+
if (!u.hasInsurance) issues.push('Need insurance coverage');
|
|
252
|
+
|
|
253
|
+
return Div({ class: 'not-eligible' }, [
|
|
254
|
+
'❌ Cannot drive legally',
|
|
255
|
+
Div(['Issues: ', issues.join(', ')])
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Practical Examples
|
|
261
|
+
|
|
262
|
+
### Form Validation with Progressive Disclosure
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const formData = Observable.object({
|
|
266
|
+
email: '',
|
|
267
|
+
password: '',
|
|
268
|
+
confirmPassword: ''
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const isValidEmail = formData.email.check(e =>
|
|
272
|
+
e.includes('@') && e.includes('.') && e.length > 5
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const isValidPassword = formData.password.check(p => p.length >= 8);
|
|
276
|
+
|
|
277
|
+
const passwordsMatch = Observable.computed(() => {
|
|
278
|
+
const data = formData.$val();
|
|
279
|
+
return data.password === data.confirmPassword && data.password.length > 0;
|
|
280
|
+
}, [formData.password, formData.confirmPassword]);
|
|
281
|
+
|
|
282
|
+
const canSubmit = Observable.computed(() =>
|
|
283
|
+
isValidEmail.val() && isValidPassword.val() && passwordsMatch.val(),
|
|
284
|
+
[isValidEmail, isValidPassword, passwordsMatch]
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const RegistrationForm = Div({ class: 'registration-form' }, [
|
|
288
|
+
// Email field with validation
|
|
289
|
+
Input({
|
|
290
|
+
type: 'email',
|
|
291
|
+
placeholder: 'Email',
|
|
292
|
+
value: formData.email
|
|
293
|
+
}),
|
|
294
|
+
ShowIf(formData.email.check(e => e.length > 0 && !isValidEmail.val()),
|
|
295
|
+
Div({ class: 'error' }, 'Please enter a valid email address')
|
|
296
|
+
),
|
|
297
|
+
|
|
298
|
+
// Password field
|
|
299
|
+
Input({
|
|
300
|
+
type: 'password',
|
|
301
|
+
placeholder: 'Password',
|
|
302
|
+
value: formData.password
|
|
303
|
+
}),
|
|
304
|
+
ShowIf(formData.password.check(p => p.length > 0 && p.length < 8),
|
|
305
|
+
Div({ class: 'error' }, 'Password must be at least 8 characters')
|
|
306
|
+
),
|
|
307
|
+
|
|
308
|
+
// Confirm password (only show after password is valid)
|
|
309
|
+
ShowIf(isValidPassword, [
|
|
310
|
+
Input({
|
|
311
|
+
type: 'password',
|
|
312
|
+
placeholder: 'Confirm Password',
|
|
313
|
+
value: formData.confirmPassword
|
|
314
|
+
}),
|
|
315
|
+
ShowIf(formData.confirmPassword.check(p => p.length > 0 && !passwordsMatch.val()),
|
|
316
|
+
Div({ class: 'error' }, 'Passwords do not match')
|
|
317
|
+
)
|
|
318
|
+
]),
|
|
319
|
+
|
|
320
|
+
// Submit button
|
|
321
|
+
Switch(canSubmit,
|
|
322
|
+
Button('Create Account').nd.on.click(() => {
|
|
323
|
+
console.log('Creating account...', formData.$val());
|
|
324
|
+
}),
|
|
325
|
+
Button({ disabled: true, class: 'disabled' }, 'Create Account')
|
|
326
|
+
)
|
|
327
|
+
]);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Progressive User Interface
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
const appState = Observable.object({
|
|
334
|
+
user: null,
|
|
335
|
+
currentView: 'welcome',
|
|
336
|
+
settings: { theme: 'light', notifications: true }
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const isLoggedIn = appState.user.check(user => user !== null);
|
|
340
|
+
const isGuest = appState.user.check(user => user === null);
|
|
341
|
+
|
|
342
|
+
const App = Div({ class: 'app' }, [
|
|
343
|
+
// Header - changes based on auth state
|
|
344
|
+
Header({ class: 'app-header' }, [
|
|
345
|
+
H1('My App'),
|
|
346
|
+
Switch(isLoggedIn,
|
|
347
|
+
// Authenticated header
|
|
348
|
+
() => Div({ class: 'user-menu' }, [
|
|
349
|
+
Span(['Welcome, ', appState.user.val().name]),
|
|
350
|
+
Button('Settings').nd.on.click(() =>
|
|
351
|
+
appState.currentView.set('settings')
|
|
352
|
+
),
|
|
353
|
+
Button('Logout').nd.on.click(() => {
|
|
354
|
+
appState.user.set(null);
|
|
355
|
+
appState.currentView.set('welcome');
|
|
356
|
+
})
|
|
357
|
+
]),
|
|
358
|
+
// Guest header
|
|
359
|
+
Div({ class: 'auth-buttons' }, [
|
|
360
|
+
Button('Sign In').nd.on.click(() =>
|
|
361
|
+
appState.currentView.set('login')
|
|
362
|
+
),
|
|
363
|
+
Button('Sign Up').nd.on.click(() =>
|
|
364
|
+
appState.currentView.set('register')
|
|
365
|
+
)
|
|
366
|
+
])
|
|
367
|
+
)
|
|
368
|
+
]),
|
|
369
|
+
|
|
370
|
+
// Main content area
|
|
371
|
+
Match(appState.currentView, {
|
|
372
|
+
welcome: WelcomeView(),
|
|
373
|
+
login: LoginView(appState),
|
|
374
|
+
register: RegisterView(appState),
|
|
375
|
+
dashboard: When(isLoggedIn)
|
|
376
|
+
.show(() => DashboardView(appState.user.val()))
|
|
377
|
+
.otherwise(() => {
|
|
378
|
+
appState.currentView.set('welcome');
|
|
379
|
+
return Div('Redirecting...');
|
|
380
|
+
}),
|
|
381
|
+
settings: When(isLoggedIn)
|
|
382
|
+
.show(() => SettingsView(appState))
|
|
383
|
+
.otherwise(() => {
|
|
384
|
+
appState.currentView.set('welcome');
|
|
385
|
+
return Div('Please log in to access settings');
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
]);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Performance Considerations
|
|
392
|
+
|
|
393
|
+
### Efficient Content Updates
|
|
394
|
+
|
|
395
|
+
NativeDocument optimizes conditional rendering by only updating the DOM when conditions actually change:
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
const isVisible = Observable(true);
|
|
399
|
+
|
|
400
|
+
// This content is created once and reused
|
|
401
|
+
const expensiveContent = ShowIf(isVisible, () => {
|
|
402
|
+
console.log('Creating expensive content'); // Only called when becoming visible
|
|
403
|
+
return createComplexComponent();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Toggling rapidly won't recreate content unnecessarily
|
|
407
|
+
isVisible.set(false);
|
|
408
|
+
isVisible.set(true); // Content is recreated
|
|
409
|
+
isVisible.set(true); // No recreation, already visible
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Memory Management
|
|
413
|
+
|
|
414
|
+
Functions in conditional rendering are called only when needed, preventing memory waste:
|
|
415
|
+
|
|
416
|
+
```javascript
|
|
417
|
+
const currentTab = Observable('home');
|
|
418
|
+
|
|
419
|
+
// Heavy components only created when their tab is active
|
|
420
|
+
const TabContent = Match(currentTab, {
|
|
421
|
+
home: () => {
|
|
422
|
+
console.log('Creating home tab');
|
|
423
|
+
return HomeTabComponent(); // Only created when needed
|
|
424
|
+
},
|
|
425
|
+
profile: () => {
|
|
426
|
+
console.log('Creating profile tab');
|
|
427
|
+
return ProfileTabComponent(); // Only created when needed
|
|
428
|
+
},
|
|
429
|
+
settings: () => {
|
|
430
|
+
console.log('Creating settings tab');
|
|
431
|
+
return SettingsTabComponent(); // Only created when needed
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Avoiding Unnecessary Computations
|
|
437
|
+
|
|
438
|
+
Use Observable.computed for expensive condition calculations:
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
const items = Observable.array([...largeDataset]);
|
|
442
|
+
const searchTerm = Observable('');
|
|
443
|
+
|
|
444
|
+
// Computed once, reused in multiple places
|
|
445
|
+
const filteredItems = Observable.computed(() => {
|
|
446
|
+
return items.val().filter(item =>
|
|
447
|
+
item.name.toLowerCase().includes(searchTerm.val().toLowerCase())
|
|
448
|
+
);
|
|
449
|
+
}, [items, searchTerm]);
|
|
450
|
+
|
|
451
|
+
const hasResults = filteredItems.check(results => results.length > 0);
|
|
452
|
+
|
|
453
|
+
const SearchResults = Div([
|
|
454
|
+
ShowIf(hasResults, () =>
|
|
455
|
+
ForEach(filteredItems, item => ItemComponent(item))
|
|
456
|
+
),
|
|
457
|
+
HideIf(hasResults, 'No results found')
|
|
458
|
+
]);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Best Practices
|
|
462
|
+
|
|
463
|
+
### 1. Choose the Right Tool
|
|
464
|
+
|
|
465
|
+
```javascript
|
|
466
|
+
// Good: Use ShowIf for simple boolean conditions
|
|
467
|
+
ShowIf(isVisible, content)
|
|
468
|
+
|
|
469
|
+
// Good: Use Switch for binary choices
|
|
470
|
+
Switch(isLoggedIn, welcomeMessage, loginPrompt)
|
|
471
|
+
|
|
472
|
+
// Good: Use Match for multiple states
|
|
473
|
+
Match(status, { loading: '...', success: '...', error: '...' })
|
|
474
|
+
|
|
475
|
+
// Less ideal: Using Match for simple boolean
|
|
476
|
+
Match(isVisible, { true: content, false: null })
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### 2. Use Functions for Dynamic Content
|
|
480
|
+
|
|
481
|
+
```javascript
|
|
482
|
+
// Good: Function creates fresh content with current values
|
|
483
|
+
ShowIf(user.check(u => u.isAdmin),
|
|
484
|
+
() => Div(`Admin: ${user.val().name}`)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
// Problematic: Static content won't update
|
|
488
|
+
ShowIf(user.check(u => u.isAdmin),
|
|
489
|
+
Div(`Admin: ${user.val().name}`) // Name won't update if user changes
|
|
490
|
+
)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### 3. Combine Conditions Logically
|
|
494
|
+
|
|
495
|
+
```javascript
|
|
496
|
+
// Good: Use computed observables for complex conditions
|
|
497
|
+
const canEdit = Observable.computed(() => {
|
|
498
|
+
const u = user.val();
|
|
499
|
+
const p = post.val();
|
|
500
|
+
return u.isLoggedIn && (u.role === 'admin' || u.id === p.authorId);
|
|
501
|
+
}, [user, post]);
|
|
502
|
+
|
|
503
|
+
ShowIf(canEdit, editButton)
|
|
504
|
+
|
|
505
|
+
// Less maintainable: Inline complex logic
|
|
506
|
+
ShowIf(user.check(u => u.isLoggedIn && u.role === 'admin'), editButton)
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### 4. Handle Edge Cases Gracefully
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
// Good: Defensive programming
|
|
513
|
+
ShowIf(data.check(d => d && d.items && d.items.length > 0),
|
|
514
|
+
() => ForEach(data.val().items, renderItem)
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
// Risky: Might throw errors on null/undefined
|
|
518
|
+
ShowIf(data.check(d => d.items.length > 0), content)
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 5. Use Meaningful Variable Names
|
|
522
|
+
|
|
523
|
+
```javascript
|
|
524
|
+
// Good: Clear intent
|
|
525
|
+
const isUserAuthenticated = user.check(u => u.token !== null);
|
|
526
|
+
const hasUnreadMessages = messages.check(m => m.some(msg => !msg.read));
|
|
527
|
+
|
|
528
|
+
ShowIf(hasUnreadMessages, notificationBadge)
|
|
529
|
+
|
|
530
|
+
// Less clear: Generic names
|
|
531
|
+
const check1 = user.check(u => u.token !== null);
|
|
532
|
+
const check2 = messages.check(m => m.some(msg => !msg.read));
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Common Patterns
|
|
536
|
+
|
|
537
|
+
### Loading States with Error Handling
|
|
538
|
+
|
|
539
|
+
```javascript
|
|
540
|
+
const requestState = Observable.object({
|
|
541
|
+
status: 'idle', // idle, loading, success, error
|
|
542
|
+
data: null,
|
|
543
|
+
error: null
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const DataView = Match(requestState.status, {
|
|
547
|
+
idle: Button('Load Data').nd.on.click(loadData),
|
|
548
|
+
loading: Div({ class: 'loading' }, 'Loading...'),
|
|
549
|
+
success: () => DataDisplay(requestState.data.val()),
|
|
550
|
+
error: () => Div({ class: 'error' }, [
|
|
551
|
+
'Error: ', requestState.error,
|
|
552
|
+
Button('Retry').nd.on.click(loadData)
|
|
553
|
+
])
|
|
554
|
+
});
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Feature Flags and Progressive Enhancement
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
const features = Observable.object({
|
|
561
|
+
darkMode: true,
|
|
562
|
+
newDashboard: false,
|
|
563
|
+
betaFeatures: false
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const App = Div([
|
|
567
|
+
// Theme switching
|
|
568
|
+
Switch(features.darkMode,
|
|
569
|
+
Div({ class: 'app dark-theme' }, content),
|
|
570
|
+
Div({ class: 'app light-theme' }, content)
|
|
571
|
+
),
|
|
572
|
+
|
|
573
|
+
// Beta features for power users
|
|
574
|
+
ShowIf(features.betaFeatures, BetaPanel()),
|
|
575
|
+
|
|
576
|
+
// A/B testing new dashboard
|
|
577
|
+
Switch(features.newDashboard,
|
|
578
|
+
NewDashboard(),
|
|
579
|
+
LegacyDashboard()
|
|
580
|
+
)
|
|
581
|
+
]);
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Debugging Conditional Rendering
|
|
585
|
+
|
|
586
|
+
### Using Console Logs in Conditions
|
|
587
|
+
|
|
588
|
+
```javascript
|
|
589
|
+
const debugCondition = condition.check(value => {
|
|
590
|
+
console.log('Condition evaluated:', value);
|
|
591
|
+
return value > 10;
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
ShowIf(debugCondition, content);
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Tracking State Changes
|
|
598
|
+
|
|
599
|
+
```javascript
|
|
600
|
+
const status = Observable('idle');
|
|
601
|
+
|
|
602
|
+
// Log all status changes
|
|
603
|
+
status.subscribe(newStatus => {
|
|
604
|
+
console.log('Status changed to:', newStatus);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Debug what content is being rendered
|
|
608
|
+
const StatusContent = Match(status, {
|
|
609
|
+
idle: () => {
|
|
610
|
+
console.log('Rendering idle state');
|
|
611
|
+
return IdleComponent();
|
|
612
|
+
},
|
|
613
|
+
loading: () => {
|
|
614
|
+
console.log('Rendering loading state');
|
|
615
|
+
return LoadingComponent();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## Next Steps
|
|
621
|
+
|
|
622
|
+
Now that you understand conditional rendering, explore these related topics:
|
|
623
|
+
|
|
624
|
+
- **[Routing](docs/routing.md)** - Navigation and URL management
|
|
625
|
+
- **[State Management](docs/state-management.md)** - Global state patterns
|
|
626
|
+
- **[Lifecycle Events](docs/lifecycle-events.md)** - Lifecycle events
|
|
627
|
+
- **[Memory Management](docs/memory-management.md)** - Memory management
|
|
628
|
+
- **[Anchor](docs/anchor.md)** - Anchor
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Contributing to NativeDocument
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to NativeDocument! We welcome all types of contributions.
|
|
4
|
+
|
|
5
|
+
## Quick Start for Contributors
|
|
6
|
+
|
|
7
|
+
1. **Fork the repository** on GitHub
|
|
8
|
+
2. **Clone your fork** locally: `git clone https://github.com/your-username/native-document.git`
|
|
9
|
+
3. **Install dependencies**: `npm install`
|
|
10
|
+
4. **Start development**: `npm run dev`
|
|
11
|
+
|
|
12
|
+
## Types of Contributions
|
|
13
|
+
|
|
14
|
+
### Bug Reports
|
|
15
|
+
Found a bug? Please include:
|
|
16
|
+
- Steps to reproduce
|
|
17
|
+
- Expected vs actual behavior
|
|
18
|
+
- Browser/environment details
|
|
19
|
+
- Minimal code example
|
|
20
|
+
|
|
21
|
+
### Feature Requests
|
|
22
|
+
Have an idea? We'd love to hear it! Please describe:
|
|
23
|
+
- The problem you're solving
|
|
24
|
+
- Your proposed solution
|
|
25
|
+
- Alternative approaches considered
|
|
26
|
+
|
|
27
|
+
### Code Contributions
|
|
28
|
+
- Fork and create a feature branch: `git checkout -b feature/amazing-feature`
|
|
29
|
+
- Test your changes manually with examples
|
|
30
|
+
- Follow our coding standards
|
|
31
|
+
- Submit a pull request with clear description
|
|
32
|
+
|
|
33
|
+
## Development Guidelines
|
|
34
|
+
|
|
35
|
+
### Code Style
|
|
36
|
+
- Use camelCase for variables and functions
|
|
37
|
+
- Add JSDoc comments for public APIs
|
|
38
|
+
- Keep functions small and focused
|
|
39
|
+
- Use meaningful variable names
|
|
40
|
+
|
|
41
|
+
### Manual Testing
|
|
42
|
+
- Test your changes with provided examples
|
|
43
|
+
- Create new examples to demonstrate features
|
|
44
|
+
- Verify cross-browser compatibility
|
|
45
|
+
|
|
46
|
+
## Documentation
|
|
47
|
+
Help improve our docs:
|
|
48
|
+
- Fix typos and unclear explanations
|
|
49
|
+
- Add missing examples
|
|
50
|
+
- Translate to other languages
|
|
51
|
+
- Create tutorial content
|