+
Skip to content

okskna/ripple

 
 

Repository files navigation

Ripple - the elegant TypeScript UI framework

CI Discord Open in StackBlitz

What is RippleJS?

Currently, this project is still in early development, and should not be used in production.

Ripple is a TypeScript UI framework that takes the best parts of React, Solid and Svelte and combines them into one package.

I wrote Ripple as a love letter for frontend web.

Personally, I (@trueadm) have been involved in some truly amazing frontend frameworks along their journeys – from Inferno, where it all began, to React and the journey of React Hooks, to creating Lexical, to Svelte 5 and its new compiler and signal-based reactivity runtime. Along that journey, I collected ideas, and intriguing thoughts that may or may not pay off. Given my time between roles, I decided it was the best opportunity to try them out, and for open source to see what I was cooking.

Ripple was designed to be a JS/TS-first framework, rather than HTML-first. Ripple modules have their own .ripple extension, and these modules fully support TypeScript. By introducing a new extension, it allows Ripple to invent its own superset language, which plays really nicely with TypeScript and JSX, but with a few interesting touches. In my experience, this has led to better DX not only for humans, but also for LLMs.

Features

  • Reactive Primitives: Built-in reactivity with track and @ reactive syntax on primitives
  • Reactive Objects: You can create fully reactive arrays/objects using shorthand syntax #[] #{}
  • Component-Based Architecture: Clean, reusable components with props and children
  • JSX-like Syntax: Familiar templating with Ripple-specific enhancements
  • Performance: Fine-grain rendering, with industry-leading performance, bundle-size and memory usage
  • TypeScript Support: Full TypeScript integration with type checking
  • VSCode Integration: Rich editor support with diagnostics, syntax highlighting, and IntelliSense
  • Prettier Support: Full Prettier formatting support for .ripple modules

Missing Features

  • SSR: Ripple is currently an SPA only. It will have SSR soon! Hydration to follow after.

Getting Started

Try Ripple

We're working hard on getting an online playground available. Watch this space!

You can try Ripple now by using our basic Vite template either via StackBlitz, or by running these commands in your terminal:

npx degit trueadm/ripple/templates/basic my-app
cd my-app
npm i # or yarn or pnpm
npm run dev # or yarn or pnpm

or use create-ripple interactive CLI tool for creating new Ripple applications with features like Tailwind CSS or Bootstrap setup.

npx create-ripple  # or yarn create ripple or pnpm create ripple

If you want to install the RippleJS package directly, it is ripple on npm:

npm i --save ripple # or yarn or pnpm

VSCode Extension

The Ripple VSCode extension provides:

  • Syntax Highlighting for .ripple files
  • Real-time Diagnostics for compilation errors
  • TypeScript Integration for type checking
  • IntelliSense for autocompletion

You can find the extension on the VS Code Marketplace as Ripple for VS Code.

You can also manually install the extension .vsix that have been manually packaged.

Mounting your app

You can use the mount API from the ripple package to render your Ripple component, using the target option to specify what DOM element you want to render the component.

// index.ts
import { mount } from 'ripple';
import { App } from '/App.ripple';

mount(App, {
  props: {
    title: 'Hello world!',
  },
  target: document.getElementById('root'),
});

Key Concepts

Components

Define reusable components with the component keyword. These are similar to functions in that they have props, but crucially, they allow for a JSX-like syntax to be defined alongside standard TypeScript. That means you do not return JSX like in other frameworks, but you instead use it like a JavaScript statement, as shown:

component Button(props: { text: string, onClick: () => void }) {
  <button onClick={props.onClick}>
    {props.text}
  </button>
}

// Usage
export component App() {
  <Button text="Click me" onClick={() => console.log("Clicked!")} />
}

Ripple's templating language also supports shorthands and object spreads too:

// you can do a normal prop
<div onClick={onClick}>{text}</div>

// or using the shorthand prop
<div {onClick}>{text}</div>

// and you can spread props
<div {...properties}>{text}</div>

Reactivity

You use track to create a single tracked value. The track function will created a boxed Tracked<V> object that is not accessible from the outside, and instead you must use @ to unbox the Tracked<V> object to read or write its underlying value. You can pass the Tracked<V> object between components, functions and context to read and write to the value in different parts of your codebase.

import { track } from 'ripple';

let name = track('World');
let count = track(0);

// Updates automatically trigger re-renders
@count++;

Objects can also contain tracked values with @ to access the reactive object property:

import { track } from 'ripple';

let counter = { current: track(0) };

// Updates automatically trigger re-renders
counter.@current++;

Tracked derived values are also Tracked<V> objects, except you pass a function to track rather than a value:

let count = track(0);
let double = track(() => @count * 2);
let quadruple = track(() => @double * 2);

console.log(@quadruple);

If you want to use a tracked value inside a reactive context, such as an effect but you don't want that value to be a tracked dependency, you can use untrack:

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));
})

Note: you cannot create Tracked objects in module/global scope, they have to be created on access from an active component context.

track with get / set

The optional get and set parameters of the track function let you customize how a tracked value is read or written, similar to property accessors but expressed as pure functions. The get function receives the current stored value and its return value is exposed when the tracked value is accessed / unboxed with @. The set function should return the value that will actually be stored and receives two parameters: the first is the one being assigned and the second with the previous value. The get and set functions may be useful for tasks such as logging, validating, or transforming values before they are exposed or stored.

import { track } from 'ripple';

export component App() {
  let count = track(0,
    (current) => {
      console.log(current);
      return current;
    },
    (next, prev) => {
      console.log(prev);
      if (typeof next === 'string') {
        next = Number(next);
      }

      return next;
    }
  );
}

Note: If no value is returned from either get or set, undefined is either exposed (for get) or stored (for set). Also, if only supplying the set, the get parameter must be set to undefined.

trackSplit Function

The trackSplit "splits" a plain object — such as component props — into specified tracked variables and an extra rest property containing the remaining unspecified object properties.

const [children, count, rest] = trackSplit(props, ['children', 'count']);

When working with component props, destructuring is often useful — both for direct use as variables and for collecting remaining properties into a rest object (which can be named arbitrarily). If destructuring happens in the component argument, e.g. component Child({ children, value, ...rest }), Ripple automatically links variable access to the original props — for example, value is compiled to props.value, preserving reactivity.

However, destructuring inside the component body, e.g. const { children, value, ...rest } = props, for read-only reactive props, does not preserve reactivity (too complicated to implement due to many edge cases). To ensure destructured read-only reactive props remain reactive in this case, use the trackSplit function.

Note: boxed / wrapped Tracked objects are always reactive since they cross function boundaries by reference. Props that were not declared with track() are never reactive and always render the same value that was initially passed in.

A full example utilizing various Ripple constructs demonstrates the split option usage:

import { track, trackSplit } from 'ripple';
import type { PropsWithChildren, Tracked } from 'ripple';

component Child(props: PropsWithChildren<{ count: Tracked<number>, className: string }>) {
  // children, count are always reactive
  // but className is passed in as a read-only reactive value
  const [children, count, className, rest] = trackSplit(props, ['children', 'count', 'class']);

  <button class={@className} {...@rest}><@children /></button>
  <pre>{`Count is: ${@count}`}</pre>
  <button onClick={() => @count++}>{'Increment Count'}</button>
}

export component App() {
    let count = track(0,
    (current) => {
      console.log('getter', current);
      return current;
    },
    (next) => {
      console.log('setter', next);
      return next;
    }
  );
  let className = track('shadow');
  let name = track('Click Me');

  function buttonRef(el) {
    console.log('ref called with', el);
    return () => {
      console.log('cleanup ref for', el);
    };
  }

  <Child
    class={@className}
    onClick={() => { @name === 'Click Me' ? @name = 'Clicked' : @name = 'Click Me'; @className = ''}}
    {count}
    {ref buttonRef}
  >{@name}</Child>;
}

With the regular destructuring, such as the one below, the class property would lose its reactivity:

// ❌ WRONG class / className reactivity would be lost
let { children, count, class: className, ...rest } = props;

Note: Make sure the resulting rest, if it's going to be spread onto a dom element, does not contain Tracked values. Otherwise, you'd be spreading not the actual values but the boxed ones, which are objects that will appear as [object Object] on the dom element.

Transporting Reactivity

Ripple doesn't constrain reactivity to components only. Tracked<V> objects can simply be passed by reference between boundaries:

import { effect, track } from 'ripple';

function createDouble(count) {
  const double = track(() => @count * 2);

  effect(() => {
    console.log('Count:', @count)
  });

  return double;
}

export component App() {
  let count = track(0);

  const double = createDouble(count);

  <div>{'Double: ' + @double}</div>
  <button onClick={() => { @count++; }}>{'Increment'}</button>
}

Dynamic Components

Ripple has built-in support for dynamic components, a way to render different components based on reactive state. Instead of hardcoding which component to show, you can store a component in a Tracked via track(), and update it at runtime. When the tracked value changes, Ripple automatically unmounts the previous component and mounts the new one. Dynamic components are written with the <@Component /> tag, where the @ both unwraps the tracked reference and tells the compiler that the component is dynamic. This makes it straightforward to pass components as props or swap them directly within a component, enabling flexible, state-driven UIs with minimal boilerplate.

export component App() {
  let swapMe = track(() => Child1);

  <Child {swapMe} />

  <button onClick={() => @swapMe = @swapMe === Child1 ? Child2 : Child1}>{'Swap Component'}</button>
}

component Child({ swapMe }: {swapMe: Tracked<Component>}) {
  <@swapMe />
}

component Child1(props) {
  <pre>{'I am child 1'}</pre>
}

component Child2(props) {
  <pre>{'I am child 2'}</pre>
}

Simple Reactive Arrays

Just like objects, you can use the Tracked<V> objects in any standard JavaScript object, like arrays:

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.

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:

export component App() {
  const items = #[1, 2, 3];

  <div>
    <p>{"Length: "}{items.length}</p>  // Reactive length
    for (const item of items) {
      <div>{item}</div>
    }
    <button onClick={() => items.push(items.length + 1)}>{"Add"}</button>
  </div>
}

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.

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:

export component App() {
  const obj = #{a: 0}

  obj.a = 0;

  <pre>{'obj.a is: '}{obj.a}</pre>
  <pre>{'obj.b is: '}{obj.b}</pre>
  <button onClick={() => { obj.a++; obj.b = obj.b ?? 5; obj.b++; }}>{'Increment'}</button>
}

Reactive Set

The TrackedSet extends the standard JS Set class, and supports all of its methods and properties.

import { TrackedSet } from 'ripple';

const set = new TrackedSet([1, 2, 3]);

TrackedSet's reactive methods or properties can be used directly or assigned to reactive variables.

import { TrackedSet, track } from 'ripple';

export component App() {
  const set = new TrackedSet([1, 2, 3]);

  // direct usage
  <p>{"Direct usage: set contains 2: "}{set.has(2)}</p>

  // reactive assignment
  let has = track(() => set.has(2));
  <p>{"Assigned usage: set contains 2: "}{@has}</p>

  <button onClick={() => set.delete(2)}>{"Delete 2"}</button>
  <button onClick={() => set.add(2)}>{"Add 2"}</button>
}

Reactive Map

The TrackedMap extends the standard JS Map class, and supports all of its methods and properties.

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.

import { TrackedMap, track } from 'ripple';

export component App() {
  const map = new TrackedMap([[1,1], [2,2], [3,3], [4,4]]);

  // direct usage
  <p>{"Direct usage: map has an item with key 2: "}{map.has(2)}</p>

  // reactive assignment
  let has = track(() => map.has(2));
  <p>{"Assigned usage: map has an item with key 2: "}{@has}</p>

  <button onClick={() => map.delete(2)}>{"Delete item with key 2"}</button>
  <button onClick={() => map.set(2, 2)}>{"Add key 2 with value 2"}</button>
}

Reactive Date

The TrackedDate extends the standard JS Date class, and supports all of its methods and properties.

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.

import { TrackedDate, track } from 'ripple';

export component App() {
  const date = new TrackedDate(2025, 0, 1, 12, 0, 0);

  // direct usage
  <p>{"Direct usage: Current year is "}{date.getFullYear()}</p>
  <p>{"ISO String: "}{date.toISOString()}</p>

  // reactive assignment
  let year = track(() => date.getFullYear());
  let month = track(() => date.getMonth());
  <p>{"Assigned usage: Year "}{@year}{", Month "}{@month}</p>

  <button onClick={() => date.setFullYear(2027)}>{"Change to 2026"}</button>
  <button onClick={() => date.setMonth(11)}>{"Change to December"}</button>
}

Effects

When dealing with reactive state, you might want to be able to create side-effects based upon changes that happen upon updates. To do this, you can use effect:

import { effect, track } from 'ripple';

export component App() {
  let count = track(0);

  effect(() => {
    console.log(@count);
  });

  <button onClick={() => @count++}>{'Increment'}</button>
}

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().

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');
    });
  });

  <button onClick={() => @count++}>{'Increment'}</button>
}

Control flow

The JSX-like syntax might take some time to get used to if you're coming from another framework. For one, templating in Ripple can only occur inside a component body – you can't create JSX inside functions, or assign it to variables as an expression.

<div>
  // you can create variables inside the template!
  const str = "hello world";

  console.log(str); // and function calls too!

  debugger; // you can put breakpoints anywhere to help debugging!

  {str}
</div>

Note that strings inside the template need to be inside {"string"}, you can't do <div>hello</div> as Ripple has no idea if hello is a string or maybe some JavaScript code that needs evaluating, so just ensure you wrap them in curly braces. This shouldn't be an issue in the real-world anyway, as you'll likely use an i18n library that means using JavaScript expressions regardless.

If statements

If blocks work seamlessly with Ripple's templating language, you can put them inside the JSX-like statements, making control-flow far easier to read and reason with.

component Truthy({ x }) {
  <div>
    if (x) {
      <span>{'x is truthy'}</span>
    } else {
      <span>{'x is falsy'}</span>
    }
  </div>
}

For statements

You can render collections using a for...of loop.

component ListView({ title, items }) {
  <h2>{title}</h2>
  <ul>
    for (const item of items) {
      <li>{item.text}</li>
    }
  </ul>
}

The for...of loop has also a built-in support for accessing the loops numerical index. The label index declares a variable that will used to assign the loop's index.

  for (const item of items; index i) {
    <div>{item}{' at index '}{i}</div>
  }

You can use Ripple's reactive arrays to easily compose contents of an array.

import { TrackedArray } from 'ripple';

component Numbers() {
  const array = new TrackedArray(1, 2, 3);

  for (const item of array; index i) {
    <div>{item}{' at index '}{i}</div>
  }

  <button onClick={() => array.push(`Item ${array.length + 1}`)}>{"Add Item"}</button>
}

Clicking the <button> will create a new item.

Note: for...of loops inside components must contain either dom elements or components. Otherwise, the loop can be run inside an effect or function.

Try statements

Try blocks work to build the foundation for error boundaries, when the runtime encounters an error in the try block, you can easily render a fallback in the catch block.

import { reportError } from 'some-library';

component ErrorBoundary() {
  <div>
    try {
      <ComponentThatFails />
    } catch (e) {
      reportError(e);

      <div>{'An error occurred! ' + e.message}</div>
    }
  </div>
}

Children

Use children prop and then use it in the form of <children /> for component composition.

When you pass in children to a component, it gets implicitly passed as the children prop, in the form of a component.

import type { Component } from 'ripple';

component Card(props: { children: Component }) {
  <div class="card">
    <props.children />
  </div>
}

// Usage
<Card>
  <p>{"Card content here"}</p>
</Card>

You could also explicitly write the same code as shown:

import type { Component } from 'ripple';

component Card(props: { children: Component }) {
  <div class="card">
    <props.children />
  </div>
}

// Usage with explicit component
<Card>
  component children() {
    <p>{"Card content here"}</p>
  }
</Card>

Refs

Ripple provides a consistent way to capture the underlying DOM element – refs. Specifically, using the syntax {ref fn} where fn is a function that captures the DOM element. If you're familiar with other frameworks, then this is identical to {@attach fn} in Svelte 5 and somewhat similar to ref in React. The hook function will receive the reference to the underlying DOM element.

export component App() {
  let div = track();

  const divRef = (node) => {
    @div = node;
    console.log("mounted", node);

    return () => {
      @div = undefined;
      console.log("unmounted", node);
    };
  };

  <div {ref divRef}>{"Hello world"}</div>
}

You can also create {ref} functions inline.

export component App() {
  let div = track();

  <div {ref (node) => {
    @div = node;
    return () => @div = undefined;
  }}>{"Hello world"}</div>
}

You can also use function factories to define properties, these are functions that return functions that do the same thing. However, you can use this pattern to pass reactive properties.

import { fadeIn } from 'some-library';

export component App({ ms }) {
  <div {ref fadeIn({ ms })}>{"Hello world"}</div>
}

Lastly, you can use refs on composite components.

<Image {ref (node) => console.log(node)} {...props} />

When passing refs to composite components (rather than HTML elements) as shown above, they will be passed a Symbol property, as they are not named. This still means that it can be spread to HTML template elements later on and still work.

createRefKey

Creates a unique object key that will be recognised as a ref when the object is spread onto an element. This allows programmatic assignment of refs without relying directly on the {ref ...} template syntax.

import { createRefKey } from 'ripple';

export component App() {
  let value = track('');

  const props = {
    id: "example",
    @value,
    [createRefKey()]: (node) => {
      const removeListener = node.addEventListener('input', (e) => @value = e.target.value);

      return () => {
        removeListener();
      }
    }
  };

  // applied to an element
  <input type="text" {...props} />

  // with composite component
  <Input {...props} />
}

component Input({ id, value, ...rest }) {
  <input type="text" {id} {value} {...rest} />
}

Raw HTML

By default, all text nodes in Ripple are escaped to prevent unintended script injections. If you'd like to render trusted HTML onto your page, you can use the HTML directive to opt-out:

export component App() {
	let source = `
<h1>My Blog Post</h1>
<p>Hi! I like JS and Ripple.</p>
`

	<article>
		{html source}
	</article>
}

Events

Event Props

Like React, events are props that start with on and then continue with an uppercase character, such as:

  • onClick
  • onPointerMove
  • onPointerDown
  • onKeyDown

For capture phase events, just add Capture to the end of the prop name:

  • onClickCapture
  • onPointerMoveCapture
  • onPointerDownCapture
  • onKeyDownCapture

However, and important distinction is that Ripple does not have a synthetic event system like React. So for example, you should opt to use onInput instead of onChange and things like onFocus and onBlur do not bubble – instead use onFocusIn and onFocusOut.

Note: Some events are automatically delegated where possible by Ripple to improve runtime performance.

on

Adds an event handler to an element and returns a function to remove it. Compared to using addEventListener directly, 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. We strongly advise to use it instead of addEventListener.

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

Ripple supports native CSS styling that is localized to the given component using the <style> element.

component MyComponent() {
  <div class="container"><h1>{'Hello World'}</h1></div>

  <style>
    .container {
      background: blue;
      padding: 1rem;
    }

    h1 {
      color: white;
      font-size: 2rem;
    }
  </style>
}

Note: the <style> element must be top-level within a component.

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:

let includeBaz = track(true);
<div class={{ foo: true, bar: false, baz: @includeBaz }}></div>
// becomes: class="foo baz"

<div class={['foo', {baz: false}, 0 && 'bar', [true && 'bat'] ]}></div>
// becomes: class="foo bat"

let count = track(3);
<div class={['foo', {bar: @count > 2}, @count > 3 && 'bat']}></div>
// 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:

let color = track('red');

<div style={`color: ${@color}; font-weight: bold; background-color: gray`}></div>
<div style={{ color: @color, fontWeight: 'bold', 'background-color': 'gray' }}></div>

const style = {
  @color,
  fontWeight: 'bold',
  'background-color': 'gray',
};

// using object spread
<div style={{...style}}></div>

// using object directly
<div style={style}></div>

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.

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:

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);

  <button onClick={() => { obj.@count++; @count2++ }}>
    {'Click Me'}
  </button>

  // context's reactive property count gets updated
  <pre>{'Context: '}{context.get().@count}</pre>
  <pre>{'Context2: '}{@count2}</pre>
}

Note: @(context2.get()) usage with @() wrapping syntax will be enabled in the near future

Passing data between components:

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!");

  <Child />
}

You can also pass a reactive Tracked<V> object through context and read it at the other side.

import { Context, effect } from 'ripple';

const MyContext = new Context(null);

component Child() {
  const count = MyContext.get();

  effect(() => {
    console.log(@count);
  });
}

component Parent() {
  const count = track(0);

  MyContext.set(count);

  <Child />

  <button onClick={() => @count++}>{"increment count"}</button>
}

Testing

We recommend using Ripple using Ripple's Vite plugin. We also recommend using Vitest for testing. When using Vitest, make sure to configure your vitest.config.js according by using this template config:

import { configDefaults, defineConfig } from 'vitest/config';
import { ripple } from 'vite-plugin-ripple';

export default defineConfig({
  plugins: [ripple()],
  resolve: process.env.VITEST ? { conditions: ['browser'] } : undefined,
  test: {
    include: ['**/*.test.ripple'],
    environment: 'jsdom',
    ...configDefaults.test,
  },
});

Then you can create a example.test.ripple module and put your Vitest test assertions in that module.

Contributing

We are happy for your interest in contributing. Please see our contributing guidelines for more information.

License

See the MIT license.

About

the elegant TypeScript UI framework

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 87.9%
  • HTML 6.4%
  • Vue 2.7%
  • TypeScript 2.2%
  • CSS 0.8%
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载