rip-lang 2.5.0 → 2.7.1
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/CHANGELOG.md +53 -10
- package/README.md +135 -245
- package/docs/BROWSER.md +8 -11
- package/docs/GUIDE.md +101 -923
- package/docs/INTERNALS.md +2 -2
- package/docs/PHILOSOPHY.md +23 -78
- package/docs/REACTIVITY.md +288 -0
- package/docs/WHY-YES-RIP.md +39 -177
- package/docs/dist/rip.browser.js +603 -2429
- package/docs/dist/rip.browser.min.js +280 -356
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/docs/repl.html +94 -437
- package/package.json +4 -1
- package/scripts/serve.js +2 -0
- package/src/compiler.js +73 -2160
- package/src/grammar/grammar.rip +22 -57
- package/src/lexer.js +11 -298
- package/src/parser.js +220 -223
- package/src/repl.js +202 -128
- package/src/tags.js +0 -62
package/docs/GUIDE.md
CHANGED
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
# Rip Language Guide
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **Modern CoffeeScript with Built-in Reactivity**
|
|
6
6
|
|
|
7
|
-
This comprehensive guide covers Rip's reactive primitives,
|
|
7
|
+
This comprehensive guide covers Rip's reactive primitives, special operators, and regex enhancements. Rip provides reactivity as a **language-level construct**, not a library import—state management is built into the syntax itself.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
13
|
1. [Reactivity](#1-reactivity)
|
|
14
|
-
2. [
|
|
15
|
-
3. [
|
|
16
|
-
4. [Special Operators](#4-special-operators)
|
|
17
|
-
5. [Regex+ Features](#5-regex-features)
|
|
14
|
+
2. [Special Operators](#2-special-operators)
|
|
15
|
+
3. [Regex+ Features](#3-regex-features)
|
|
18
16
|
|
|
19
17
|
---
|
|
20
18
|
|
|
@@ -24,72 +22,91 @@ Rip provides reactive primitives as **language-level operators**, not library im
|
|
|
24
22
|
|
|
25
23
|
## Reactive Operators
|
|
26
24
|
|
|
27
|
-
| Operator | Name | Purpose |
|
|
28
|
-
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
25
|
+
| Operator | Name | Read as | Purpose |
|
|
26
|
+
|----------|------|---------|---------|
|
|
27
|
+
| `=` | Assign | "gets value" | Regular assignment |
|
|
28
|
+
| `:=` | State | "**has state**" | Reactive state variable |
|
|
29
|
+
| `~=` | Computed | "**always equals**" | Computed value (auto-updates when dependencies change) |
|
|
30
|
+
| `~>` | Effect | "**reacts to**" | Side effect that runs when dependencies change |
|
|
31
|
+
| `=!` | Readonly | "equals, dammit!" | Constant (`const`) - not reactive, just immutable |
|
|
33
32
|
|
|
34
|
-
## Reactive State (`:=`)
|
|
33
|
+
## Reactive State (`:=`) — "has state"
|
|
35
34
|
|
|
36
|
-
The
|
|
35
|
+
The state operator creates reactive state:
|
|
37
36
|
|
|
38
37
|
```coffee
|
|
39
|
-
count := 0 #
|
|
40
|
-
name := "world" #
|
|
38
|
+
count := 0 # count has state 0
|
|
39
|
+
name := "world" # name has state "world"
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
State changes automatically trigger updates in any
|
|
42
|
+
State changes automatically trigger updates in any computed values or effects that depend on them.
|
|
44
43
|
|
|
45
|
-
##
|
|
44
|
+
## Computed Values (`~=`) — "always equals"
|
|
46
45
|
|
|
47
|
-
The
|
|
46
|
+
The computed operator creates a value that automatically recomputes when its dependencies change:
|
|
48
47
|
|
|
49
48
|
```coffee
|
|
50
49
|
count := 0
|
|
51
|
-
doubled ~= count * 2 #
|
|
50
|
+
doubled ~= count * 2 # doubled always equals count * 2
|
|
52
51
|
|
|
53
52
|
count = 5 # doubled automatically becomes 10
|
|
54
53
|
count = 10 # doubled automatically becomes 20
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
##
|
|
56
|
+
## Side Effects (`~>`) — "reacts to"
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
The effect operator defines a side effect that runs when its dependencies change. Dependencies are auto-tracked from reactive values read in the body:
|
|
60
59
|
|
|
61
60
|
```coffee
|
|
62
|
-
|
|
63
|
-
host = "localhost"
|
|
64
|
-
host = "example.com" # OK - variables are flexible by default
|
|
61
|
+
count := 0
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
API_URL =! "https://api.example.com"
|
|
68
|
-
MAX_RETRIES =! 3
|
|
63
|
+
~> console.log "Count changed to:", count
|
|
69
64
|
|
|
70
|
-
|
|
65
|
+
count = 5 # Logs: "Count changed to: 5"
|
|
66
|
+
count = 10 # Logs: "Count changed to: 10"
|
|
71
67
|
```
|
|
72
68
|
|
|
73
|
-
|
|
69
|
+
Effects are useful for:
|
|
70
|
+
- Logging and debugging
|
|
71
|
+
- Syncing with external systems
|
|
72
|
+
- Analytics tracking
|
|
73
|
+
- Local storage persistence
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
### Controllable Effects
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
Assign the effect to a variable to control it:
|
|
78
78
|
|
|
79
79
|
```coffee
|
|
80
80
|
count := 0
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
# Fire and forget (no assignment)
|
|
83
|
+
~> console.log count
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
# Controllable (assign to variable)
|
|
86
|
+
logger ~> console.log count
|
|
87
|
+
|
|
88
|
+
logger.stop! # Pause reactions
|
|
89
|
+
logger.run! # Resume reactions
|
|
90
|
+
logger.cancel! # Permanent disposal
|
|
86
91
|
```
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
## Constant Values (`=!`) — "equals, dammit!"
|
|
94
|
+
|
|
95
|
+
In Rip, regular assignment (`=`) compiles to `let` for maximum flexibility. When you want an immutable constant, use the "equal, dammit!" operator (`=!`), which compiles to `const`:
|
|
96
|
+
|
|
97
|
+
```coffee
|
|
98
|
+
# Regular assignment → let (can reassign)
|
|
99
|
+
host = "localhost"
|
|
100
|
+
host = "example.com" # OK - variables are flexible by default
|
|
101
|
+
|
|
102
|
+
# Equal, dammit! → const (can't reassign)
|
|
103
|
+
API_URL =! "https://api.example.com"
|
|
104
|
+
MAX_RETRIES =! 3
|
|
105
|
+
|
|
106
|
+
API_URL = "other" # Error! const cannot be reassigned
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This gives you opt-in immutability when you need it, while keeping the default flexible for scripting.
|
|
93
110
|
|
|
94
111
|
## Auto-Unwrapping
|
|
95
112
|
|
|
@@ -114,11 +131,22 @@ count.read() # Get value without tracking dependencies
|
|
|
114
131
|
|--------|---------|
|
|
115
132
|
| `x.read()` | Get value without tracking (for effects that shouldn't re-run) |
|
|
116
133
|
| `x.value` | Direct access to the underlying value |
|
|
117
|
-
| `+x` | Shorthand for `x.value` (triggers tracking in effects) |
|
|
134
|
+
| `+x` | Shorthand for `x.value` (triggers tracking in computed/effects) |
|
|
118
135
|
| `x.lock()` | Make value readonly (can read but can't change) |
|
|
119
|
-
| `x.free()` | Unsubscribe from all dependencies (
|
|
136
|
+
| `x.free()` | Unsubscribe from all dependencies (state still works) |
|
|
120
137
|
| `x.kill()` | Clean up everything and return final value |
|
|
121
138
|
|
|
139
|
+
## Effect Controller Methods
|
|
140
|
+
|
|
141
|
+
When you assign an effect to a variable, you get a controller object:
|
|
142
|
+
|
|
143
|
+
| Method | Purpose |
|
|
144
|
+
|--------|---------|
|
|
145
|
+
| `e.stop!` | Pause reactions (can resume later) |
|
|
146
|
+
| `e.run!` | Resume reactions |
|
|
147
|
+
| `e.cancel!` | Permanent disposal (cannot resume) |
|
|
148
|
+
| `e.active` | Boolean — is the effect running? |
|
|
149
|
+
|
|
122
150
|
## Dependency Tracking
|
|
123
151
|
|
|
124
152
|
Understanding when dependencies are tracked is key to effective reactive programming.
|
|
@@ -133,7 +161,7 @@ Understanding when dependencies are tracked is key to effective reactive program
|
|
|
133
161
|
| `+count` | ✅ Yes | Unary plus triggers `.valueOf()` |
|
|
134
162
|
| `count.value` | ✅ Yes | Direct `.value` access |
|
|
135
163
|
| `count.read()` | ❌ No | Explicit non-tracking read |
|
|
136
|
-
| `y = count` | ❌ No | Assigns
|
|
164
|
+
| `y = count` | ❌ No | Assigns state object, not value |
|
|
137
165
|
|
|
138
166
|
### Example: Tracking vs Non-Tracking
|
|
139
167
|
|
|
@@ -141,37 +169,17 @@ Understanding when dependencies are tracked is key to effective reactive program
|
|
|
141
169
|
count := 10
|
|
142
170
|
|
|
143
171
|
# Effect A: Subscribes to count (will re-run when count changes)
|
|
144
|
-
|
|
172
|
+
~> console.log "A: #{count}"
|
|
145
173
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
count = 20
|
|
150
|
-
# Output:
|
|
151
|
-
# A: 20 ← Effect A re-ran
|
|
152
|
-
# ← Effect B did NOT re-run
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### When to Use `.read()`
|
|
156
|
-
|
|
157
|
-
Use `.read()` when you need the current value but don't want to create a dependency:
|
|
158
|
-
|
|
159
|
-
```coffee
|
|
160
|
-
count := 0
|
|
161
|
-
lastSaved := 0
|
|
162
|
-
|
|
163
|
-
effect ->
|
|
164
|
-
# We want to log count changes, but compare against lastSaved
|
|
165
|
-
# without re-running when lastSaved changes
|
|
166
|
-
if count != lastSaved.read()
|
|
167
|
-
console.log "Unsaved changes: #{count}"
|
|
174
|
+
# Reading without tracking (for comparisons, etc.)
|
|
175
|
+
currentValue = count.read() # Does not create dependency
|
|
168
176
|
```
|
|
169
177
|
|
|
170
178
|
## Lifecycle & Cleanup
|
|
171
179
|
|
|
172
|
-
### Locking a
|
|
180
|
+
### Locking a State
|
|
173
181
|
|
|
174
|
-
Make a
|
|
182
|
+
Make a state readonly (subscriptions stay active):
|
|
175
183
|
|
|
176
184
|
```coffee
|
|
177
185
|
config := { theme: "dark" }
|
|
@@ -193,25 +201,29 @@ doubled.free() # No longer updates when count changes
|
|
|
193
201
|
count = 10 # doubled stays at its last value
|
|
194
202
|
```
|
|
195
203
|
|
|
196
|
-
### Killing a
|
|
204
|
+
### Killing a State
|
|
197
205
|
|
|
198
206
|
Clean up completely and get the final value:
|
|
199
207
|
|
|
200
208
|
```coffee
|
|
201
209
|
count := 10
|
|
202
|
-
finalValue = count.kill() # Returns 10,
|
|
210
|
+
finalValue = count.kill() # Returns 10, state is now dead
|
|
203
211
|
|
|
204
|
-
count = 20 # Error or no-op (
|
|
212
|
+
count = 20 # Error or no-op (state is dead)
|
|
205
213
|
```
|
|
206
214
|
|
|
207
215
|
### Effect Cleanup
|
|
208
216
|
|
|
209
|
-
|
|
217
|
+
Use the effect controller to manage lifecycle:
|
|
210
218
|
|
|
211
219
|
```coffee
|
|
212
|
-
|
|
220
|
+
# Assign to a variable for control
|
|
221
|
+
ticker ~>
|
|
213
222
|
interval = setInterval (-> tick()), 1000
|
|
214
|
-
-> clearInterval interval # Cleanup
|
|
223
|
+
-> clearInterval interval # Cleanup function returned
|
|
224
|
+
|
|
225
|
+
# Later, when done:
|
|
226
|
+
ticker.cancel! # Stops the effect and runs cleanup
|
|
215
227
|
```
|
|
216
228
|
|
|
217
229
|
## Real-World Example
|
|
@@ -219,21 +231,17 @@ effect ->
|
|
|
219
231
|
A complete reactive counter with persistence:
|
|
220
232
|
|
|
221
233
|
```coffee
|
|
222
|
-
# Reactive state
|
|
234
|
+
# Reactive state — count has state (loaded from localStorage)
|
|
223
235
|
count := parseInt(localStorage.getItem("count")) or 0
|
|
224
236
|
|
|
225
|
-
#
|
|
237
|
+
# Computed values — always equal to their expressions
|
|
226
238
|
doubled ~= count * 2
|
|
227
239
|
isEven ~= count % 2 == 0
|
|
228
240
|
message ~= "Count is #{count} (#{isEven ? 'even' : 'odd'})"
|
|
229
241
|
|
|
230
|
-
# Side
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# Side effect: log changes
|
|
235
|
-
effect ->
|
|
236
|
-
console.log message
|
|
242
|
+
# Side effects — react to dependencies (auto-tracked)
|
|
243
|
+
~> localStorage.setItem "count", count # Persist
|
|
244
|
+
~> console.log message # Log
|
|
237
245
|
|
|
238
246
|
# Usage
|
|
239
247
|
count = 5
|
|
@@ -251,16 +259,16 @@ The Rip compiler transforms reactive operators into efficient JavaScript:
|
|
|
251
259
|
|
|
252
260
|
```coffee
|
|
253
261
|
# Rip source
|
|
254
|
-
count := 0
|
|
255
|
-
doubled ~= count * 2
|
|
256
|
-
|
|
262
|
+
count := 0 # count has state 0
|
|
263
|
+
doubled ~= count * 2 # doubled always equals count * 2
|
|
264
|
+
~> console.log count # reacts to count changes
|
|
257
265
|
```
|
|
258
266
|
|
|
259
267
|
```javascript
|
|
260
268
|
// Compiled output (conceptual)
|
|
261
|
-
const count =
|
|
269
|
+
const count = __state(0);
|
|
262
270
|
const doubled = __computed(() => count.value * 2);
|
|
263
|
-
__effect(() => console.log(
|
|
271
|
+
__effect(() => { console.log(count.value); });
|
|
264
272
|
```
|
|
265
273
|
|
|
266
274
|
The runtime is **automatically inlined** - no external dependencies required.
|
|
@@ -289,843 +297,15 @@ console.log(y);
|
|
|
289
297
|
| Concept | React | Vue | Solid | Rip |
|
|
290
298
|
|---------|-------|-----|-------|-----|
|
|
291
299
|
| State | `useState()` | `ref()` | `createSignal()` | `x := 0` |
|
|
292
|
-
|
|
|
293
|
-
| Effect | `useEffect()` | `watch()` | `createEffect()` | `
|
|
300
|
+
| Computed | `useMemo()` | `computed()` | `createMemo()` | `x ~= y * 2` |
|
|
301
|
+
| Effect | `useEffect()` | `watch()` | `createEffect()` | `~> body` or `x ~> body` |
|
|
294
302
|
| Constant | `const` | `const` | `const` | `x =! 0` |
|
|
295
303
|
|
|
296
304
|
Rip's approach: **No imports, no hooks, no special functions. Just operators.**
|
|
297
305
|
|
|
298
306
|
---
|
|
299
307
|
|
|
300
|
-
# 2.
|
|
301
|
-
|
|
302
|
-
Rip's template syntax is not a separate language—it's native Rip syntax. The `render` block uses indentation-based markup that compiles directly to efficient DOM operations.
|
|
303
|
-
|
|
304
|
-
## Quick Reference
|
|
305
|
-
|
|
306
|
-
| Syntax | Purpose | Example |
|
|
307
|
-
|--------|---------|---------|
|
|
308
|
-
| `tag` | Element | `div`, `span`, `button` |
|
|
309
|
-
| `.class` | CSS class | `div.card`, `button.btn.primary` |
|
|
310
|
-
| `#id` | Element ID | `div#main`, `section#hero` |
|
|
311
|
-
| `.()` | Dynamic classes | `div.('active', isOn && 'on')` |
|
|
312
|
-
| `attr: val` | Attribute | `type: "text"`, `disabled: true` |
|
|
313
|
-
| `@event: fn` | Event handler | `@click: handleClick` |
|
|
314
|
-
| `@event.mod:` | Event modifier | `@click.prevent: submit` |
|
|
315
|
-
| `"text"` | Text content | `span "Hello"` |
|
|
316
|
-
| `#{expr}` | Interpolation | `"Count: #{count}"` |
|
|
317
|
-
| `ref: var` | Element reference | `ref: inputEl` |
|
|
318
|
-
| `key: val` | List item key | `key: item.id` |
|
|
319
|
-
| `...props` | Spread attributes | `div ...props` |
|
|
320
|
-
| `X <=> var` | Two-way binding | `value <=> name` |
|
|
321
|
-
| `if`/`else` | Conditional | `div if visible` |
|
|
322
|
-
| `for...in` | Iteration | `for item in items` |
|
|
323
|
-
|
|
324
|
-
## Tags
|
|
325
|
-
|
|
326
|
-
### Basic Tags
|
|
327
|
-
|
|
328
|
-
```coffee
|
|
329
|
-
render
|
|
330
|
-
div
|
|
331
|
-
span
|
|
332
|
-
button
|
|
333
|
-
input
|
|
334
|
-
MyComponent
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### With Classes (CSS Selector Syntax)
|
|
338
|
-
|
|
339
|
-
```coffee
|
|
340
|
-
div.card
|
|
341
|
-
div.card.active
|
|
342
|
-
button.btn.btn-primary
|
|
343
|
-
span.badge.badge-success
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
### With IDs
|
|
347
|
-
|
|
348
|
-
```coffee
|
|
349
|
-
div#main
|
|
350
|
-
section#hero
|
|
351
|
-
input#search-field
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Combined
|
|
355
|
-
|
|
356
|
-
```coffee
|
|
357
|
-
section#hero.full-width.dark
|
|
358
|
-
div#sidebar.panel.collapsed
|
|
359
|
-
article#post-123.blog-post.featured
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
## Attributes
|
|
363
|
-
|
|
364
|
-
### Inline
|
|
365
|
-
|
|
366
|
-
```coffee
|
|
367
|
-
input type: "text", placeholder: "Enter name", required: true
|
|
368
|
-
a href: "/home", target: "_blank", "Go Home"
|
|
369
|
-
img src: user.avatar, alt: user.name
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### Indented (for many attributes)
|
|
373
|
-
|
|
374
|
-
```coffee
|
|
375
|
-
input
|
|
376
|
-
type: "email"
|
|
377
|
-
placeholder: "you@example.com"
|
|
378
|
-
required: true
|
|
379
|
-
autocomplete: "email"
|
|
380
|
-
@input: handleInput
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Dynamic Values
|
|
384
|
-
|
|
385
|
-
```coffee
|
|
386
|
-
input value: searchTerm, disabled: isLoading
|
|
387
|
-
img src: user.avatar, alt: user.name
|
|
388
|
-
a href: "/users/#{user.id}", "View Profile"
|
|
389
|
-
div title: tooltip, data-id: item.id
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Boolean Attributes
|
|
393
|
-
|
|
394
|
-
Boolean attributes are present when `true`, absent when `false`:
|
|
395
|
-
|
|
396
|
-
```coffee
|
|
397
|
-
button disabled: isLoading # <button disabled> or <button>
|
|
398
|
-
input required: true, readonly: isLocked
|
|
399
|
-
option selected: isDefault
|
|
400
|
-
details open: expanded
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### Dynamic Classes with `cx()` (clsx-compatible)
|
|
404
|
-
|
|
405
|
-
Rip includes a `clsx`-compatible `cx()` helper for dynamic class composition. Use the `.()` syntax on elements:
|
|
406
|
-
|
|
407
|
-
```coffee
|
|
408
|
-
# Basic: conditions in parens
|
|
409
|
-
div.('card', isActive && 'active', size)
|
|
410
|
-
|
|
411
|
-
# With static classes too
|
|
412
|
-
div.card.('highlighted', isNew && 'new')
|
|
413
|
-
|
|
414
|
-
# Object syntax (like clsx)
|
|
415
|
-
div.({ active: isActive, disabled: isDisabled })
|
|
416
|
-
|
|
417
|
-
# Mixed - strings, conditions, objects
|
|
418
|
-
div.base.('extra', { selected: isSelected })
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### Spreading Props
|
|
422
|
-
|
|
423
|
-
Spread an object as attributes:
|
|
424
|
-
|
|
425
|
-
```coffee
|
|
426
|
-
render
|
|
427
|
-
# Basic spread
|
|
428
|
-
div ...props
|
|
429
|
-
|
|
430
|
-
# With static classes
|
|
431
|
-
div.card ...props
|
|
432
|
-
|
|
433
|
-
# With explicit attrs (override spreads)
|
|
434
|
-
input ...inputProps, class: "extra", disabled: true
|
|
435
|
-
|
|
436
|
-
# With children
|
|
437
|
-
div.wrapper ...containerProps
|
|
438
|
-
span "Content"
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
## Two-Way Binding
|
|
442
|
-
|
|
443
|
-
Two-way binding automatically syncs an element's value with a variable using the `<=>` operator:
|
|
444
|
-
|
|
445
|
-
```coffee
|
|
446
|
-
render
|
|
447
|
-
# Text input - value syncs with username
|
|
448
|
-
input value <=> username
|
|
449
|
-
|
|
450
|
-
# Checkbox - checked syncs with isActive
|
|
451
|
-
input type: "checkbox", checked <=> isActive
|
|
452
|
-
|
|
453
|
-
# Select dropdown
|
|
454
|
-
select value <=> selectedId
|
|
455
|
-
|
|
456
|
-
# Textarea
|
|
457
|
-
textarea value <=> content
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
The `<=>` operator reads as "syncs with" — it's a visual representation of bidirectional data flow.
|
|
461
|
-
|
|
462
|
-
**Smart event selection:**
|
|
463
|
-
- `value` on `input`/`textarea` → `oninput` event
|
|
464
|
-
- `value` on `select` → `onchange` event
|
|
465
|
-
- `checked` → `onchange` event
|
|
466
|
-
|
|
467
|
-
**Smart value access:**
|
|
468
|
-
- `type="number"` inputs → `e.target.valueAsNumber`
|
|
469
|
-
- `type="range"` inputs → `e.target.valueAsNumber`
|
|
470
|
-
- All other inputs → `e.target.value`
|
|
471
|
-
|
|
472
|
-
## Event Handlers
|
|
473
|
-
|
|
474
|
-
### Basic Events
|
|
475
|
-
|
|
476
|
-
```coffee
|
|
477
|
-
button @click: handleClick
|
|
478
|
-
button @click: -> count += 1
|
|
479
|
-
input @input: (e) -> value = e.target.value
|
|
480
|
-
form @submit: handleSubmit
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### Event Handler Patterns
|
|
484
|
-
|
|
485
|
-
There are two common patterns for event handlers:
|
|
486
|
-
|
|
487
|
-
```coffee
|
|
488
|
-
# Normal: define methods, reference with @
|
|
489
|
-
inc: -> count += 1
|
|
490
|
-
button @click: @inc, "+"
|
|
491
|
-
|
|
492
|
-
# Compact: inline with fat arrow (parens required)
|
|
493
|
-
button (@click: => @count++), "+"
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
The fat arrow (`=>`) binds `this` correctly for inline handlers.
|
|
497
|
-
|
|
498
|
-
### Event Modifiers
|
|
499
|
-
|
|
500
|
-
```coffee
|
|
501
|
-
# Prevent default
|
|
502
|
-
form @submit.prevent: handleSubmit
|
|
503
|
-
a @click.prevent: navigate
|
|
504
|
-
|
|
505
|
-
# Stop propagation
|
|
506
|
-
button @click.stop: handleClick
|
|
507
|
-
|
|
508
|
-
# Combined
|
|
509
|
-
a @click.prevent.stop: handleNavigation
|
|
510
|
-
|
|
511
|
-
# Once (auto-removes after first call)
|
|
512
|
-
button @click.once: initialize
|
|
513
|
-
|
|
514
|
-
# Self (only if target is the element itself)
|
|
515
|
-
div @click.self: handleDivClick
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
### Key Modifiers
|
|
519
|
-
|
|
520
|
-
```coffee
|
|
521
|
-
input @keydown.enter: submit
|
|
522
|
-
input @keydown.escape: cancel
|
|
523
|
-
input @keydown.tab: handleTab
|
|
524
|
-
input @keydown.space: togglePlay
|
|
525
|
-
input @keydown.up: previousItem
|
|
526
|
-
input @keydown.down: nextItem
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
### Modifier Key Combinations
|
|
530
|
-
|
|
531
|
-
```coffee
|
|
532
|
-
input @keydown.ctrl.s: save
|
|
533
|
-
input @keydown.cmd.s: save # Mac Command key
|
|
534
|
-
input @keydown.shift.enter: newLine
|
|
535
|
-
input @keydown.ctrl.shift.z: redo
|
|
536
|
-
button @click.ctrl: openInNewTab
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
## Text Content
|
|
540
|
-
|
|
541
|
-
### As Final Argument
|
|
542
|
-
|
|
543
|
-
```coffee
|
|
544
|
-
button "Click me"
|
|
545
|
-
span "Hello, #{name}!"
|
|
546
|
-
h1 "Welcome"
|
|
547
|
-
p "This is a paragraph of text."
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
### Variables as Text
|
|
551
|
-
|
|
552
|
-
```coffee
|
|
553
|
-
span count
|
|
554
|
-
span user.name
|
|
555
|
-
span formatCurrency(total)
|
|
556
|
-
td item.quantity
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
### Mixed Content
|
|
560
|
-
|
|
561
|
-
```coffee
|
|
562
|
-
p
|
|
563
|
-
"Hello, "
|
|
564
|
-
strong name
|
|
565
|
-
"! Welcome back."
|
|
566
|
-
|
|
567
|
-
span
|
|
568
|
-
"Total: "
|
|
569
|
-
strong formatCurrency(total)
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
## Children & Nesting
|
|
573
|
-
|
|
574
|
-
Rip uses implicit nesting based on indentation:
|
|
575
|
-
|
|
576
|
-
```coffee
|
|
577
|
-
div.card
|
|
578
|
-
header.card-header
|
|
579
|
-
h2.title "Product"
|
|
580
|
-
span.badge "New"
|
|
581
|
-
|
|
582
|
-
div.card-body
|
|
583
|
-
p description
|
|
584
|
-
|
|
585
|
-
ul.features
|
|
586
|
-
li "Feature one"
|
|
587
|
-
li "Feature two"
|
|
588
|
-
li "Feature three"
|
|
589
|
-
|
|
590
|
-
footer.card-footer
|
|
591
|
-
button.secondary "Cancel"
|
|
592
|
-
button.primary "Buy Now"
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
You can also use explicit arrow syntax for inline nesting:
|
|
596
|
-
|
|
597
|
-
```coffee
|
|
598
|
-
div.card -> h1 "Title"
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
## Conditionals
|
|
602
|
-
|
|
603
|
-
### If/Else Blocks
|
|
604
|
-
|
|
605
|
-
```coffee
|
|
606
|
-
div.status
|
|
607
|
-
if loading
|
|
608
|
-
span.spinner
|
|
609
|
-
"Loading..."
|
|
610
|
-
else if error
|
|
611
|
-
span.error error.message
|
|
612
|
-
else
|
|
613
|
-
span.success "Loaded!"
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
### Inline Conditionals
|
|
617
|
-
|
|
618
|
-
```coffee
|
|
619
|
-
span.badge "Admin" if user.isAdmin
|
|
620
|
-
span.warning "Unsaved" unless saved
|
|
621
|
-
div.alert error if error
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
### Ternary Expressions
|
|
625
|
-
|
|
626
|
-
```coffee
|
|
627
|
-
span class: { active: isActive }
|
|
628
|
-
isActive ? "On" : "Off"
|
|
629
|
-
|
|
630
|
-
button class: { primary: isPrimary }
|
|
631
|
-
isPrimary ? "Save" : "Continue"
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
## Loops
|
|
635
|
-
|
|
636
|
-
### Array Iteration with Key
|
|
637
|
-
|
|
638
|
-
**Always provide a `key` for list items** to enable efficient updates:
|
|
639
|
-
|
|
640
|
-
```coffee
|
|
641
|
-
ul.todo-list
|
|
642
|
-
for todo in todos, key: todo.id
|
|
643
|
-
li class: { completed: todo.done }
|
|
644
|
-
span todo.text
|
|
645
|
-
button @click: -> remove(todo), "×"
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
### With Index
|
|
649
|
-
|
|
650
|
-
```coffee
|
|
651
|
-
ol
|
|
652
|
-
for item, i in items, key: item.id
|
|
653
|
-
li "#{i + 1}. #{item.name}"
|
|
654
|
-
|
|
655
|
-
table
|
|
656
|
-
for row, rowIndex in rows, key: row.id
|
|
657
|
-
tr class: { even: rowIndex % 2 is 0 }
|
|
658
|
-
for cell, colIndex in row.cells, key: colIndex
|
|
659
|
-
td cell
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
### Object Iteration
|
|
663
|
-
|
|
664
|
-
```coffee
|
|
665
|
-
dl
|
|
666
|
-
for key, value of user
|
|
667
|
-
dt key
|
|
668
|
-
dd value
|
|
669
|
-
|
|
670
|
-
div.metadata
|
|
671
|
-
for prop, val of item.meta
|
|
672
|
-
span.tag "#{prop}: #{val}"
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### Ranges
|
|
676
|
-
|
|
677
|
-
```coffee
|
|
678
|
-
# Numeric range
|
|
679
|
-
ul
|
|
680
|
-
for i in [1..5]
|
|
681
|
-
li "Item #{i}"
|
|
682
|
-
|
|
683
|
-
# Dynamic range
|
|
684
|
-
ul
|
|
685
|
-
for page in [1..totalPages], key: page
|
|
686
|
-
button @click: -> goToPage(page), page
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
## Refs
|
|
690
|
-
|
|
691
|
-
Element references for direct DOM access:
|
|
692
|
-
|
|
693
|
-
```coffee
|
|
694
|
-
component SearchBox
|
|
695
|
-
inputEl = null
|
|
696
|
-
|
|
697
|
-
mounted: ->
|
|
698
|
-
inputEl.focus()
|
|
699
|
-
|
|
700
|
-
clear: ->
|
|
701
|
-
inputEl.value = ""
|
|
702
|
-
inputEl.focus()
|
|
703
|
-
|
|
704
|
-
render
|
|
705
|
-
div.search
|
|
706
|
-
input ref: inputEl, type: "text", @input: handleInput
|
|
707
|
-
button @click: clear, "Clear"
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
## SVG Support
|
|
711
|
-
|
|
712
|
-
```coffee
|
|
713
|
-
svg viewBox: "0 0 24 24", width: 24, height: 24
|
|
714
|
-
path d: "M12 2L2 7l10 5 10-5-10-5z"
|
|
715
|
-
path d: "M2 17l10 5 10-5"
|
|
716
|
-
|
|
717
|
-
# With dynamic values
|
|
718
|
-
svg.icon class: { active: isActive }
|
|
719
|
-
circle cx: 12, cy: 12, r: radius
|
|
720
|
-
line x1: 0, y1: 0, x2: 24, y2: 24, stroke: color
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
## Fragment (Multiple Root Elements)
|
|
724
|
-
|
|
725
|
-
When you need multiple root elements without a wrapper:
|
|
726
|
-
|
|
727
|
-
```coffee
|
|
728
|
-
render
|
|
729
|
-
<>
|
|
730
|
-
Header
|
|
731
|
-
main
|
|
732
|
-
@children
|
|
733
|
-
Footer
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
---
|
|
737
|
-
|
|
738
|
-
# 3. Components
|
|
739
|
-
|
|
740
|
-
Rip provides component syntax as **language-level constructs**, not library patterns.
|
|
741
|
-
|
|
742
|
-
## Basic Component
|
|
743
|
-
|
|
744
|
-
```coffee
|
|
745
|
-
component HelloWorld
|
|
746
|
-
render
|
|
747
|
-
div "Hello, World!"
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
That's it. No imports, no boilerplate, no `export default`.
|
|
751
|
-
|
|
752
|
-
### Creating & Mounting
|
|
753
|
-
|
|
754
|
-
```coffee
|
|
755
|
-
# Ruby-style constructor (Rip enhancement)
|
|
756
|
-
app = HelloWorld.new()
|
|
757
|
-
app.mount "#app"
|
|
758
|
-
|
|
759
|
-
# Or chain it
|
|
760
|
-
HelloWorld.new().mount "#app"
|
|
761
|
-
|
|
762
|
-
# Traditional JS style also works
|
|
763
|
-
app = new HelloWorld()
|
|
764
|
-
app.mount "#app"
|
|
765
|
-
|
|
766
|
-
# With props
|
|
767
|
-
Counter.new(label: "Score", initial: 10).mount "#counter"
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
The `mount` method accepts either an element or a CSS selector string.
|
|
771
|
-
|
|
772
|
-
## Component Structure
|
|
773
|
-
|
|
774
|
-
```coffee
|
|
775
|
-
component Name
|
|
776
|
-
# ═══════════════════════════════════════════
|
|
777
|
-
# Constants (readonly)
|
|
778
|
-
# ═══════════════════════════════════════════
|
|
779
|
-
MAX_ITEMS =! 100
|
|
780
|
-
|
|
781
|
-
# ═══════════════════════════════════════════
|
|
782
|
-
# Props (from parent)
|
|
783
|
-
# ═══════════════════════════════════════════
|
|
784
|
-
@title # Required prop
|
|
785
|
-
@subtitle? # Optional prop (undefined if not provided)
|
|
786
|
-
@count = 0 # Optional prop with default
|
|
787
|
-
@onSelect # Callback prop
|
|
788
|
-
@children # Nested content (slot)
|
|
789
|
-
@...rest # Rest props (capture remaining)
|
|
790
|
-
|
|
791
|
-
# ═══════════════════════════════════════════
|
|
792
|
-
# State (local, reactive)
|
|
793
|
-
# ═══════════════════════════════════════════
|
|
794
|
-
expanded = false
|
|
795
|
-
items = []
|
|
796
|
-
searchTerm = ""
|
|
797
|
-
|
|
798
|
-
# ═══════════════════════════════════════════
|
|
799
|
-
# Derived (always equals)
|
|
800
|
-
# ═══════════════════════════════════════════
|
|
801
|
-
filtered ~= items.filter (i) -> i.active
|
|
802
|
-
total ~= items.reduce ((sum, i) -> sum + i.price), 0
|
|
803
|
-
isEmpty ~= items.length is 0
|
|
804
|
-
|
|
805
|
-
# ═══════════════════════════════════════════
|
|
806
|
-
# Methods (private)
|
|
807
|
-
# ═══════════════════════════════════════════
|
|
808
|
-
add: (item) ->
|
|
809
|
-
items = [...items, item]
|
|
810
|
-
|
|
811
|
-
remove: (item) ->
|
|
812
|
-
items = items.filter (i) -> i isnt item
|
|
813
|
-
|
|
814
|
-
# ═══════════════════════════════════════════
|
|
815
|
-
# Exposed Methods (parent can call)
|
|
816
|
-
# ═══════════════════════════════════════════
|
|
817
|
-
clear: ∞>
|
|
818
|
-
items = []
|
|
819
|
-
|
|
820
|
-
focus: ∞>
|
|
821
|
-
inputEl.focus()
|
|
822
|
-
|
|
823
|
-
# ═══════════════════════════════════════════
|
|
824
|
-
# Lifecycle
|
|
825
|
-
# ═══════════════════════════════════════════
|
|
826
|
-
mounted: ->
|
|
827
|
-
# After first render, DOM available
|
|
828
|
-
saved = localStorage.getItem "items"
|
|
829
|
-
items = JSON.parse saved if saved
|
|
830
|
-
|
|
831
|
-
unmounted: ->
|
|
832
|
-
# Cleanup before removal
|
|
833
|
-
|
|
834
|
-
updated: ->
|
|
835
|
-
# After any reactive update
|
|
836
|
-
|
|
837
|
-
# ═══════════════════════════════════════════
|
|
838
|
-
# Effects (side effects)
|
|
839
|
-
# ═══════════════════════════════════════════
|
|
840
|
-
effect ->
|
|
841
|
-
# Runs when dependencies change
|
|
842
|
-
localStorage.setItem "items", JSON.stringify items
|
|
843
|
-
|
|
844
|
-
effect ->
|
|
845
|
-
# Return function for cleanup
|
|
846
|
-
interval = setInterval (-> tick()), 1000
|
|
847
|
-
-> clearInterval interval
|
|
848
|
-
|
|
849
|
-
# ═══════════════════════════════════════════
|
|
850
|
-
# Render
|
|
851
|
-
# ═══════════════════════════════════════════
|
|
852
|
-
render
|
|
853
|
-
div.container
|
|
854
|
-
h1 @title
|
|
855
|
-
p @subtitle if @subtitle
|
|
856
|
-
# ... template
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
## Props System
|
|
860
|
-
|
|
861
|
-
### Declaration
|
|
862
|
-
|
|
863
|
-
```coffee
|
|
864
|
-
component Button
|
|
865
|
-
# Required (error if not provided)
|
|
866
|
-
@label
|
|
867
|
-
|
|
868
|
-
# Optional (undefined if not provided)
|
|
869
|
-
@icon?
|
|
870
|
-
|
|
871
|
-
# Optional with default
|
|
872
|
-
@variant = "default"
|
|
873
|
-
@size = "md"
|
|
874
|
-
@disabled = false
|
|
875
|
-
|
|
876
|
-
# Callback prop
|
|
877
|
-
@onClick
|
|
878
|
-
|
|
879
|
-
# Children (nested content)
|
|
880
|
-
@children
|
|
881
|
-
|
|
882
|
-
# Rest props (capture all others)
|
|
883
|
-
@...rest
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
### Usage
|
|
887
|
-
|
|
888
|
-
```coffee
|
|
889
|
-
# Parent component
|
|
890
|
-
render
|
|
891
|
-
Button
|
|
892
|
-
label: "Save"
|
|
893
|
-
variant: "primary"
|
|
894
|
-
onClick: handleSave
|
|
895
|
-
|
|
896
|
-
Button label: "Cancel", variant: "ghost", onClick: handleCancel
|
|
897
|
-
|
|
898
|
-
Button label: "Delete", variant: "danger"
|
|
899
|
-
icon: "trash" # Named prop
|
|
900
|
-
span "Are you sure?" # Becomes @children
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
### Prop Rules
|
|
904
|
-
|
|
905
|
-
| Rule | Description |
|
|
906
|
-
|------|-------------|
|
|
907
|
-
| **Readonly** | Props cannot be reassigned inside component |
|
|
908
|
-
| **Required** | `@prop` without default throws if not provided |
|
|
909
|
-
| **Optional** | `@prop?` is undefined if not provided |
|
|
910
|
-
| **Default** | `@prop = value` uses value if not provided |
|
|
911
|
-
| **Callback** | Functions passed as props, call with `@onClick()` |
|
|
912
|
-
| **Children** | `@children` receives unnamed nested content |
|
|
913
|
-
| **Rest** | `@...rest` captures all non-declared props |
|
|
914
|
-
| **Spread** | `...@rest` spreads captured props to element |
|
|
915
|
-
|
|
916
|
-
### Forwarding Props
|
|
917
|
-
|
|
918
|
-
```coffee
|
|
919
|
-
component FancyInput
|
|
920
|
-
@label
|
|
921
|
-
@error?
|
|
922
|
-
@...inputProps
|
|
923
|
-
|
|
924
|
-
render
|
|
925
|
-
div.field
|
|
926
|
-
label @label
|
|
927
|
-
input ...@inputProps # Spread all other props to input
|
|
928
|
-
span.error @error if @error
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
## Lifecycle Hooks
|
|
932
|
-
|
|
933
|
-
```coffee
|
|
934
|
-
component DataView
|
|
935
|
-
@url
|
|
936
|
-
data = null
|
|
937
|
-
error = null
|
|
938
|
-
loading = true
|
|
939
|
-
|
|
940
|
-
mounted: ->
|
|
941
|
-
# Runs once after first render
|
|
942
|
-
# DOM is available
|
|
943
|
-
try
|
|
944
|
-
data = fetch! @url
|
|
945
|
-
catch e
|
|
946
|
-
error = e.message
|
|
947
|
-
finally
|
|
948
|
-
loading = false
|
|
949
|
-
|
|
950
|
-
unmounted: ->
|
|
951
|
-
# Runs before component is removed
|
|
952
|
-
# Cleanup subscriptions, timers, etc.
|
|
953
|
-
|
|
954
|
-
updated: ->
|
|
955
|
-
# Runs after any reactive update
|
|
956
|
-
console.log "Component updated"
|
|
957
|
-
|
|
958
|
-
render
|
|
959
|
-
div
|
|
960
|
-
if loading
|
|
961
|
-
Spinner()
|
|
962
|
-
else if error
|
|
963
|
-
ErrorMessage message: error
|
|
964
|
-
else
|
|
965
|
-
DataDisplay data: data
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
| Hook | When | Use For |
|
|
969
|
-
|------|------|---------|
|
|
970
|
-
| `mounted:` | After first render | Initial fetch, DOM access, setup |
|
|
971
|
-
| `unmounted:` | Before removal | Cleanup timers, subscriptions |
|
|
972
|
-
| `updated:` | After reactive updates | Logging, analytics |
|
|
973
|
-
|
|
974
|
-
## Children / Slots
|
|
975
|
-
|
|
976
|
-
### Basic Children
|
|
977
|
-
|
|
978
|
-
```coffee
|
|
979
|
-
component Card
|
|
980
|
-
@title
|
|
981
|
-
@children
|
|
982
|
-
|
|
983
|
-
render
|
|
984
|
-
div.card
|
|
985
|
-
h2 @title
|
|
986
|
-
div.card-body
|
|
987
|
-
@children # Render children here
|
|
988
|
-
|
|
989
|
-
# Usage:
|
|
990
|
-
Card title: "My Card"
|
|
991
|
-
p "This is the card content."
|
|
992
|
-
p "It can have multiple elements."
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
### Named Slots
|
|
996
|
-
|
|
997
|
-
```coffee
|
|
998
|
-
component Layout
|
|
999
|
-
@header?
|
|
1000
|
-
@footer?
|
|
1001
|
-
@children
|
|
1002
|
-
|
|
1003
|
-
render
|
|
1004
|
-
div.layout
|
|
1005
|
-
header @header if @header
|
|
1006
|
-
main @children
|
|
1007
|
-
footer @footer if @footer
|
|
1008
|
-
|
|
1009
|
-
# Usage:
|
|
1010
|
-
Layout
|
|
1011
|
-
header:
|
|
1012
|
-
h1 "My App"
|
|
1013
|
-
nav ...
|
|
1014
|
-
footer:
|
|
1015
|
-
p "© 2024"
|
|
1016
|
-
|
|
1017
|
-
# Default content → @children
|
|
1018
|
-
p "Main content here"
|
|
1019
|
-
```
|
|
1020
|
-
|
|
1021
|
-
## Context API
|
|
1022
|
-
|
|
1023
|
-
Pass data down through component trees without prop drilling.
|
|
1024
|
-
|
|
1025
|
-
```coffee
|
|
1026
|
-
component App
|
|
1027
|
-
# Set context in constructor (runs during component init)
|
|
1028
|
-
mounted: ->
|
|
1029
|
-
setContext "theme", { dark: true, primary: "#3b82f6" }
|
|
1030
|
-
|
|
1031
|
-
render
|
|
1032
|
-
div
|
|
1033
|
-
Header()
|
|
1034
|
-
Content()
|
|
1035
|
-
Footer()
|
|
1036
|
-
|
|
1037
|
-
component Header
|
|
1038
|
-
# Get context from any ancestor
|
|
1039
|
-
theme = getContext "theme"
|
|
1040
|
-
|
|
1041
|
-
render
|
|
1042
|
-
header.("bg-blue-500" if theme?.dark)
|
|
1043
|
-
h1 "My App"
|
|
1044
|
-
|
|
1045
|
-
component DeepNestedChild
|
|
1046
|
-
# Works at any depth!
|
|
1047
|
-
theme = getContext "theme"
|
|
1048
|
-
|
|
1049
|
-
render
|
|
1050
|
-
div style: "color: #{theme?.primary}"
|
|
1051
|
-
"Themed content"
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
**API:**
|
|
1055
|
-
|
|
1056
|
-
| Function | Description |
|
|
1057
|
-
|----------|-------------|
|
|
1058
|
-
| `setContext(key, value)` | Set a context value in current component |
|
|
1059
|
-
| `getContext(key)` | Get context from nearest ancestor (or undefined) |
|
|
1060
|
-
| `hasContext(key)` | Check if context exists in any ancestor |
|
|
1061
|
-
|
|
1062
|
-
## Complete Example
|
|
1063
|
-
|
|
1064
|
-
```coffee
|
|
1065
|
-
component TodoApp
|
|
1066
|
-
# Constants
|
|
1067
|
-
STORAGE_KEY =! "todos"
|
|
1068
|
-
|
|
1069
|
-
# State
|
|
1070
|
-
todos = []
|
|
1071
|
-
newTodo = ""
|
|
1072
|
-
filter = "all"
|
|
1073
|
-
|
|
1074
|
-
# Derived
|
|
1075
|
-
filtered ~= switch filter
|
|
1076
|
-
when "active" then todos.filter (t) -> not t.done
|
|
1077
|
-
when "completed" then todos.filter (t) -> t.done
|
|
1078
|
-
else todos
|
|
1079
|
-
|
|
1080
|
-
remaining ~= todos.filter((t) -> not t.done).length
|
|
1081
|
-
allDone ~= todos.length > 0 and remaining is 0
|
|
1082
|
-
|
|
1083
|
-
# Methods
|
|
1084
|
-
add: ->
|
|
1085
|
-
return unless newTodo.trim()
|
|
1086
|
-
todos = [...todos, { id: Date.now(), text: newTodo.trim(), done: false }]
|
|
1087
|
-
newTodo = ""
|
|
1088
|
-
|
|
1089
|
-
toggle: (todo) ->
|
|
1090
|
-
todo.done = not todo.done
|
|
1091
|
-
todos = [...todos] # Trigger reactivity
|
|
1092
|
-
|
|
1093
|
-
# Lifecycle
|
|
1094
|
-
mounted: ->
|
|
1095
|
-
saved = localStorage.getItem STORAGE_KEY
|
|
1096
|
-
todos = JSON.parse saved if saved
|
|
1097
|
-
|
|
1098
|
-
# Effects
|
|
1099
|
-
effect ->
|
|
1100
|
-
localStorage.setItem STORAGE_KEY, JSON.stringify todos
|
|
1101
|
-
|
|
1102
|
-
# Render
|
|
1103
|
-
render
|
|
1104
|
-
section.todoapp
|
|
1105
|
-
header.header
|
|
1106
|
-
h1 "todos"
|
|
1107
|
-
input.new-todo
|
|
1108
|
-
placeholder: "What needs to be done?"
|
|
1109
|
-
value: newTodo
|
|
1110
|
-
@input: (e) -> newTodo = e.target.value
|
|
1111
|
-
@keydown.enter: add
|
|
1112
|
-
|
|
1113
|
-
section.main if todos.length
|
|
1114
|
-
ul.todo-list
|
|
1115
|
-
for todo in filtered, key: todo.id
|
|
1116
|
-
li class: { completed: todo.done }
|
|
1117
|
-
input.toggle type: "checkbox", checked: todo.done, @change: -> toggle todo
|
|
1118
|
-
label todo.text
|
|
1119
|
-
|
|
1120
|
-
footer.footer if todos.length
|
|
1121
|
-
span.todo-count
|
|
1122
|
-
strong remaining
|
|
1123
|
-
" items left"
|
|
1124
|
-
```
|
|
1125
|
-
|
|
1126
|
-
---
|
|
1127
|
-
|
|
1128
|
-
# 4. Special Operators
|
|
308
|
+
# 2. Special Operators
|
|
1129
309
|
|
|
1130
310
|
## Dammit Operator (`!`)
|
|
1131
311
|
|
|
@@ -1280,7 +460,7 @@ result = riskyOperation() !? "default"
|
|
|
1280
460
|
|
|
1281
461
|
---
|
|
1282
462
|
|
|
1283
|
-
#
|
|
463
|
+
# 3. Regex+ Features
|
|
1284
464
|
|
|
1285
465
|
**Ruby-Inspired Regex Matching with Automatic Capture**
|
|
1286
466
|
|
|
@@ -1433,9 +613,7 @@ text =~ /line2/m # Works! (/m flag allows newlines)
|
|
|
1433
613
|
3. **Minimal boilerplate** — No `useState`, no `.value` in most cases
|
|
1434
614
|
4. **Familiar feel** — Looks like regular assignment, behaves reactively
|
|
1435
615
|
5. **Zero dependencies** — Runtime is inlined, no external packages needed
|
|
1436
|
-
6. **
|
|
1437
|
-
7. **Templates are code** — The `render` block is Rip syntax, not a separate template language
|
|
1438
|
-
8. **Everything is reactive** — State, derived values, and effects just work
|
|
616
|
+
6. **Framework-agnostic** — Use Rip's reactivity with any UI framework
|
|
1439
617
|
|
|
1440
618
|
---
|
|
1441
619
|
|