}) {
<@swapMe />
}
component Child1(props) {
{'I am child 1'}
}
component Child2(props) {
{'I am child 2'}
}
```
**Transport Rules:**
- Reactive state must be connected to a component
- Cannot be global or created at module/global scope
- Use arrays `[ trackedVar ]` or objects `{ trackedVar }` to transport reactivity
- Functions can accept and return reactive state using these patterns
- This enables composable reactive logic outside of component boundaries
### Control Flow
#### If Statements
```ripple
component Conditional({ isVisible }) {
if (isVisible) {
{"Visible content"}
} else {
{"Hidden state"}
}
}
```
#### For Loops
```ripple
component List({ items }) {
for (const item of items) {
{item.text}
}
}
```
#### For Loops with index
```ripple
component ListView({ title, items }) {
{title}
for (const item of items; index i) {
{item.text}{' at index '}{i}
}
}
```
#### Try-Catch (Error Boundaries)
```ripple
component ErrorBoundary() {
try {
} catch (e) {
{"Error: "}{e.message}
}
}
```
### Children Components
Use `children` prop for component composition:
```ripple
import type { Component } from 'ripple';
component Card(props: { children: Component }) {
}
// Usage
{"Card content here"}
```
### Events
#### Attribute Event Handling
Events follow React-style naming (`onClick`, `onPointerMove`, etc.):
```ripple
component EventExample() {
let message = track("");
}
```
For capture phase events, add `Capture` suffix:
- `onClickCapture`
- `onPointerDownCapture`
#### Direct Event Handling
Use function `on` to attach events to window, document or any other element instead of addEventListener.
This method guarantees the proper execution order with respect to attribute-based handlers such as `onClick`, and similarly optimized through event delegation for those events that support it.
```ripple
import { effect, on } from 'ripple';
export component App() {
effect(() => {
// on component mount
const removeListener = on(window, 'resize', () => {
console.log('Window resized!');
});
// return the removeListener when the component unmounts
return removeListener;
});
}
```
### Styling
Components support scoped CSS with `
}
```
#### Dynamic Classes
In Ripple, the `class` attribute can accept more than just a string — it also supports objects and arrays. Truthy values are included as class names, while falsy values are omitted. This behavior is powered by the `clsx` library.
Examples:
```ripple
let includeBaz = track(true);
// becomes: class="foo baz"
// becomes: class="foo bat"
let count = track(3);
2}, @count > 3 && 'bat']}>
// becomes: class="foo bar"
```
#### Dynamic Inline Styles
Sometimes you might need to dynamically set inline styles. For this, you can use the `style` attribute, passing either a string or an object to it:
```ripple
let color = track('red');
const style = {
@color,
fontWeight: 'bold',
'background-color': 'gray',
};
// using object spread
// using object directly
```
Both examples above will render the same inline styles, however, it's recommended to use the object notation as it's typically more performance optimized.
> Note: When passing an object to the `style` attribute, you can use either camelCase or kebab-case for CSS property names.
### DOM References (Refs)
Use `{ref fn}` syntax to capture DOM element references:
```ripple
export component App() {
let div = track();
const divRef = (node) => {
@div = node;
console.log("mounted", node);
return () => {
@div = undefined;
console.log("unmounted", node);
};
};
{"Hello world"}
}
```
Inline refs:
```ripple
console.log(node)}>{"Content"}
```
## Built-in APIs
### Core Functions
```typescript
import {
mount, // Mount component to DOM
track, // Create reactive state
untrack, // Prevent reactivity tracking
flushSync, // Synchronous state updates
effect, // Side effects
Context // Context API
} from 'ripple';
```
### Mount API
```typescript
mount(App, {
props: { title: 'Hello world!' },
target: document.getElementById('root')
});
```
### Effects
```ripple
import { effect, track } from 'ripple';
export component App() {
let count = track(0);
effect(() => {
console.log("Count changed:", @count);
});
@count++}>{"Increment"}
}
```
### After Update tick()
The `tick()` function returns a Promise that resolves after all pending reactive updates have been applied to the DOM. This is useful when you need to ensure that DOM changes are complete before executing subsequent code, similar to Vue's `nextTick()` or Svelte's `tick()`.
```ripple
import { effect, track, tick } from 'ripple';
export component App() {
let count = track(0);
effect(() => {
@count;
if (@count === 0) {
console.log('initial run, skipping');
return;
}
tick().then(() => {
console.log('after the update');
});
});
@count++}>{'Increment'}
}
```
### Context
Ripple has the concept of `context` where a value or reactive object can be shared through the component tree –
like in other frameworks. This all happens from the `Context` class that is imported from `ripple`.
Creating contexts may take place anywhere. Contexts can contain anything including tracked values or objects. However, context cannot be read via `get` or written to via `set` inside an event handler or at the module level as it must happen within the context of a component. A good strategy is to assign the contents of a context to a variable via the `.get()` method during the component initialization and use this variable for reading and writing.
When Child components overwrite a context's value via `.set()`, this new value will only be seen by its descendants. Components higher up in the tree will continue to see the original value.
Example with tracked / reactive contents:
```ripple
import { track, Context } from "ripple"
// create context with an empty object
const context = new Context({});
const context2 = new Context();
export component App() {
// get reference to the object
const obj = context.get();
// set your reactive value
obj.count = track(0);
// create another tracked variable
const count2 = track(0);
// context2 now contains a trackrf variable
context2.set(count2);
{ obj.@count++; @count2++ }}>
{'Click Me'}
// context's reactive property count gets updated
{'Context: '}{context.get().@count}
{'Context2: '}{@count2}
}
```
> Note: `@(context2.get())` usage with `@()` wrapping syntax will be enabled in the near future
Passing data between components:
```ripple
import { Context } from 'ripple';
const MyContext = new Context(null);
component Child() {
// Context is read in the Child component
const value = MyContext.get();
// value is "Hello from context!"
console.log(value);
}
component Parent() {
const value = MyContext.get();
// Context is read in the Parent component, but hasn't yet
// been set, so we fallback to the initial context value.
// So the value is `null`
console.log(value);
// Context is set in the Parent component
MyContext.set("Hello from context!");
}
```
### Reactive Collections
#### Simple Reactive Array
Just like objects, you can use the `Tracked` objects in any standard JavaScript object, like arrays:
```ripple
let first = track(0);
let second = track(0);
const arr = [first, second];
const total = track(() => arr.reduce((a, b) => a + @b, 0));
console.log(@total);
```
Like shown in the above example, you can compose normal arrays with reactivity and pass them through props or boundaries.
However, if you need the entire array to be fully reactive, including when new elements get added, you should use the reactive array that Ripple provides.
#### Fully Reactive Array
`TrackedArray` class from Ripple extends the standard JS `Array` class, and supports all of its methods and properties. Import it from the `'ripple'` namespace or use the provided syntactic sugar for a quick creation via the bracketed notation. All elements existing or new of the `TrackedArray` are reactive and respond to the various array operations such as push, pop, shift, unshift, etc. Even if you reference a non-existent element, once it added, the original reference will react to the change. You do NOT need to use the unboxing `@` with the elements of the array.
```ripple
import { TrackedArray } from 'ripple';
// using syntactic sugar `#`
const arr = #[1, 2, 3];
// using the new constructor
const arr = new TrackedArray(1, 2, 3);
// using static from method
const arr = TrackedArray.from([1, 2, 3]);
// using static of method
const arr = TrackedArray.of(1, 2, 3);
```
Usage Example:
```ripple
export component App() {
const items = new #[1, 2, 3];
{"Length: "}{items.length}
// Reactive length
for (const item of items) {
{item}
}
items.push(items.length + 1)}>{"Add"}
}
```
#### Reactive Object
`TrackedObject` class extends the standard JS `Object` class, and supports all of its methods and properties. Import it from the `'ripple'` namespace or use the provided syntactic sugar for a quick creation via the curly brace notation. `TrackedObject` fully supports shallow reactivity and any property on the root level is reactive. You can even reference non-existent properties and once added the original reference reacts to the change. You do NOT need to use the unboxing `@` with the properties of the `TrackedObject`.
```ripple
import { TrackedObject } from 'ripple';
// using syntactic sugar `#`
const arr = #{a: 1, b: 2, c: 3};
// using the new constructor
const arr = new TrackedObject({a: 1, b: 2, c: 3});
```
Usage Example:
```ripple
export component App() {
const obj = #{a: 0}
obj.a = 0;
{'obj.a is: '}{obj.a}
{'obj.b is: '}{obj.b}
{ obj.a++; obj.b = obj.b ?? 5; obj.b++; }}>{'Increment'}
}
```
#### Reactive Set
```ripple
import { TrackedSet } from 'ripple';
component SetExample() {
const mySet = new TrackedSet([1, 2, 3]);
{"Size: "}{mySet.size}
// Reactive size
{"Has 2: "}{mySet.has(2)}
mySet.add(4)}>{"Add 4"}
}
```
#### Reactive Map
The `TrackedMap` extends the standard JS `Map` class, and supports all of its methods and properties.
```ripple
import { TrackedMap, track } from 'ripple';
const map = new TrackedMap([[1,1], [2,2], [3,3], [4,4]]);
```
TrackedMap's reactive methods or properties can be used directly or assigned to reactive variables.
```ripple
import { TrackedMap, track } from 'ripple';
export component App() {
const map = new TrackedMap([[1,1], [2,2], [3,3], [4,4]]);
// direct usage
{"Direct usage: map has an item with key 2: "}{map.has(2)}
// reactive assignment
let has = track(() => map.has(2));
{"Assigned usage: map has an item with key 2: "}{@has}
map.delete(2)}>{"Delete item with key 2"}
map.set(2, 2)}>{"Add key 2 with value 2"}
}
```
#### Reactive Date
The `TrackedDate` extends the standard JS `Date` class, and supports all of its methods and properties.
```ripple
import { TrackedDate } from 'ripple';
const date = new TrackedDate(2026, 0, 1); // January 1, 2026
```
TrackedDate's reactive methods or properties can be used directly or assigned to reactive variables. All getter methods (`getFullYear()`, `getMonth()`, `getDate()`, etc.) and formatting methods (`toISOString()`, `toDateString()`, etc.) are reactive and will update when the date is modified.
```ripple
import { TrackedDate, track } from 'ripple';
export component App() {
const date = new TrackedDate(2025, 0, 1, 12, 0, 0);
// direct usage
{"Direct usage: Current year is "}{date.getFullYear()}
{"ISO String: "}{date.toISOString()}
// reactive assignment
let year = track(() => date.getFullYear());
let month = track(() => date.getMonth());
{"Assigned usage: Year "}{@year}{", Month "}{@month}
date.setFullYear(2027)}>{"Change to 2026"}
date.setMonth(11)}>{"Change to December"}
}
```
## Advanced Features
### Untracking Reactivity
```ripple
import { untrack, track, effect } from 'ripple';
let count = track(0);
let double = track(() => @count * 2);
let quadruple = track(() => @double * 2);
effect(() => {
// This effect will never fire again, as we've untracked the only dependency it has
console.log(untrack(() => @quadruple));
})
```
### Prop Shortcuts
```ripple
// Object spread
{"Content"}
// Shorthand props (when variable name matches prop name)
{"Content"}
// Equivalent to:
{"Content"}
```
### Raw HTML
All text nodes are escaped by default in Ripple. To render trusted raw HTML
strings, use the `{html}` directive.
```ripple
export component App() {
let source = `
My Blog Post
Hi! I like JS and Ripple.
`
{html source}
}
```
## TypeScript Integration
### Component Types
```typescript
import type { Component } from 'ripple';
interface Props {
value: string;
label: string;
children?: Component;
}
component MyComponent(props: Props) {
// Component implementation
}
```
### Context Types
```typescript
type Theme = 'light' | 'dark';
const ThemeContext = new Context('light');
```
## File Structure
```
src/
App.ripple # Main app component
components/
Button.ripple # Reusable components
Card.ripple
index.ts # Entry point with mount()
```
## Development Tools
### VSCode Extension
- **Name**: "Ripple for VS Code"
- **ID**: `ripplejs.ripple-vscode-plugin`
- **Features**: Syntax highlighting, diagnostics, TypeScript integration, IntelliSense
### Vite Plugin
```typescript
// vite.config.js
import { defineConfig } from 'vite';
import ripple from 'vite-plugin-ripple';
export default defineConfig({
plugins: [ripple()]
});
```
### Prettier Plugin
```javascript
// .prettierrc
{
"plugins": ["prettier-plugin-ripple"]
}
```
## Key Differences from Other Frameworks
### vs React
- No JSX functions/returns - components use statement-based templates
- Built-in reactivity with `track` and `@` syntax instead of useState/useEffect
- Scoped CSS without CSS-in-JS libraries
- No virtual DOM - fine-grained reactivity
### vs Svelte
- TypeScript-first approach
- JSX-like syntax instead of HTML templates
- `.ripple` extension instead of `.svelte`
- Similar reactivity concepts but different syntax
### vs Solid
- Component definition with `component` keyword
- Built-in collections (TrackedArray, TrackedSet)
- Different templating approach within component bodies
## Best Practices
1. **Reactivity**: Use `track()` to create reactive variables and `@` to access them
2. **Strings**: Wrap string literals in `{"string"}` within templates
3. **Effects**: Use `effect()` for side effects, not direct reactive variable access
4. **Components**: Keep components focused and use TypeScript interfaces for props
5. **Styling**: Use scoped `