}) {
<@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"}
}
}
```
#### Switch Statements
Switch statements in Ripple provide a powerful way to conditionally render content based on the value of an expression. They are fully reactive and integrate seamlessly with Ripple's templating syntax.
**Key Features:**
- **Reactivity:** Works with both static and reactive (`Tracked`) values.
- **Strict Control Flow:** Requires explicit `break` statements to prevent fall-through (except for default and empty cases).
- **Template Integration:** `case` blocks can contain any valid Ripple template content, including other components, elements, and logic.
**Basic Usage:**
The `switch` statement evaluates an expression and matches its value against a series of `case` clauses.
```ripple
component StatusIndicator({ status }) {
// The switch statement evaluates the 'status' prop
switch (status) {
case: 'init':
case 'loading':
{'Loading...'}
break; // break is mandatory
case 'success':
{'Success!'}
break;
case 'error':
{'Error!'}
break;
default:
{'Unknown status'}
// No break needed for default
}
}
```
`switch` statements can also react to changes in `Tracked` variables. When the tracked variable changes, the `switch` statement will re-evaluate and render the appropriate `case`.
```ripple
import { track } from 'ripple';
component InteractiveStatus() {
let status = track('loading'); // Reactive state
@status = 'success'}>{'Set to Success'}
@status = 'error'}>{'Set to Error'}
// This switch block will update automatically when '@status' changes
switch (@status) {
case 'init':
case 'loading':
{'Status: Loading...'}
break;
case 'success':
{'Status: Success!'}
break;
case 'error':
{'Status: Error!'}
break;
default:
{'Status: Unknown'}
}
}
```
**⚠️ Critical: Mandatory `break`**
- **`break` is required:** You **must** include a `break` statement at the end of each `case` block (except for `default` and empty cases).
- **Compilation Error:** If you omit the `break` statement, the Ripple compiler will throw an error.
```ripple
// ❌ WRONG - Missing 'break' will cause a compilation error
switch (value) {
case 1:
{'Case 1'}
case 2:
{'Case 2'}
break;
}
```
This strict requirement ensures that control flow is always explicit and predictable.
#### 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}
}
}
```
#### For Loops with key
```ripple
component ListView({ title, items }) {
{title}
for (const item of items; index i; key item.id) {
{item.text}{' at index '}{i}
}
}
```
**Key Usage Guidelines:**
- **Arrays with `#{}` objects**: Keys are usually unnecessary - object identity and reactivity handle updates automatically. Identity-based loops are more efficient with less bookkeeping.
- **Arrays with plain objects**: Keys are needed when object reference isn't sufficient for identification. Use stable identifiers: `key item.id`.
#### Dynamic Elements
```ripple
export component App() {
let tag = track('div');
<@tag class="dynamic">{'Hello World'}@tag>
@tag = @tag === 'div' ? 'span' : 'div'}>{'Toggle Element'}
}
```
#### 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
### React Compatibility
Ripple provides a compatibility layer for integrating with React applications. This allows you to:
- Embed React components inside Ripple applications
- Embed Ripple components inside React applications
- Share React Context and React Suspense between React and Ripple components
**Note**: React SSR is not currently supported. The compatibility layer is client-side only.
#### Installation
```bash
npm install @ripple-ts/compat-react
```
#### Using React Components in Ripple (tsx:react)
The `` block allows you to embed React JSX directly inside Ripple components. React components inside these blocks use React's JSX semantics (e.g., `className` instead of `class`).
```ripple
import { Suspense } from 'react';
component App() {
{"Ripple App"}
{/* Embed React components using tsx:react */}
This is React JSX!
}
```
#### Setting Up React Compat with mount()
To use `` blocks, you must configure the React compatibility layer when mounting your Ripple app:
```typescript
// main.ts
import { mount } from 'ripple';
import { createReactCompat } from '@ripple-ts/compat-react';
import { App } from './App.ripple';
mount(App, {
target: document.getElementById('app')!,
compat: {
react: createReactCompat(),
},
});
```
#### Using Ripple Components in React (RippleRoot + Ripple)
To embed Ripple components inside a React application, wrap your React app with `` and use the `` component to render Ripple components:
```tsx
// App.tsx - React application
import { createRoot } from 'react-dom/client';
import { RippleRoot, Ripple } from '@ripple-ts/compat-react';
import { MyRippleComponent } from './MyComponent.ripple';
function App() {
return (
Hello from React!
);
}
const root = createRoot(document.getElementById('root')!);
root.render(
);
```
The `` component accepts:
- `component`: The Ripple component to render
- `props` (optional): Props to pass to the Ripple component
#### React Context Integration
React Context works seamlessly across the Ripple/React boundary. Context providers in React are accessible from Ripple components embedded via ``, and vice versa.
```ripple
import { createContext, useContext } from 'react';
// Create a React context
const ThemeContext = createContext('light');
// React component that uses context
function ThemedButton() {
const theme = useContext(ThemeContext);
return Themed Button ;
}
// Ripple component that provides and consumes React context
component App() {
}
```
#### Error Boundaries
Ripple's `try/catch` blocks can catch errors thrown by React components inside `` blocks:
```ripple
function BuggyReactComponent() {
throw new Error('Something went wrong!');
}
component App() {
try {
} catch (error) {
{"An error occurred in the React component"}
}
}
```
#### Common Mistakes with React Compatibility
**❌ WRONG: Using React JSX syntax outside tsx:react blocks**
```ripple
component App() {
// Wrong: className is React syntax, use class in Ripple
{"Hello"}
}
```
**✅ CORRECT: Use Ripple syntax outside tsx:react, React syntax inside**
```ripple
component App() {
{"Hello"}
React content
}
```
**❌ WRONG: Defining React components with JSX inside .ripple files**
```ripple
// Wrong: JSX in a .ripple file is Ripple syntax, not React syntax
function ReactChild() {
return Child
; // This is Ripple JSX, not React JSX!
}
component App() {
}
```
**✅ CORRECT: Define React components in separate .tsx files**
```tsx
// ReactChild.tsx - React component in its own file
export function ReactChild() {
return Child
; // This is React JSX
}
```
```ripple
// App.ripple - Import and use the React component
import { ReactChild } from './ReactChild.tsx';
component App() {
}
```
**✅ CORRECT: Or use jsx/jsxs directly in .ripple files**
```ripple
import { jsx } from 'react/jsx-runtime';
// React component using jsx() instead of JSX syntax
function ReactChild() {
return jsx('div', { children: 'Child' });
}
component App() {
}
```
**❌ WRONG: Forgetting to wrap React app with RippleRoot**
```tsx
// Wrong: Ripple component won't work without RippleRoot
root.render( );
```
**✅ CORRECT: Always wrap with RippleRoot when using Ripple in React**
```tsx
root.render(
);
```
**❌ WRONG: Forgetting createReactCompat() when using tsx:react in Ripple**
```typescript
// Wrong: tsx:react blocks won't render
mount(App, { target: document.getElementById('app')! });
```
**✅ CORRECT: Always configure compat when using tsx:react**
```typescript
mount(App, {
target: document.getElementById('app')!,
compat: {
react: createReactCompat(),
},
});
```
### Portal Component
The `Portal` component allows you to render (teleport) content anywhere in the DOM tree, breaking out of the normal component hierarchy. This is particularly useful for modals, tooltips, and notifications.
```ripple
import { Portal } from 'ripple';
export component App() {
{'My App'}
{/* This will render inside document.body, not inside the .app div */}
{'I am rendered in document.body!'}
{'This content escapes the normal component tree.'}
}
```
### 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**: `ripple-ts.vscode-plugin`
- **Features**: Syntax highlighting, diagnostics, TypeScript integration, IntelliSense
### Vite Plugin
```typescript
// vite.config.js
import { defineConfig } from 'vite';
import ripple from '@ripple-ts/vite-plugin';
export default defineConfig({
plugins: [ripple()]
});
```
### Prettier Plugin
```javascript
// .prettierrc
{
"plugins": ["@ripple-ts/prettier-plugin"]
}
```
## 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 `