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.
@@ -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