@@ -151,8 +151,8 @@ async function Fallback({timeout = 1000, children}) {
return children;
}
-async function *Suspense({timeout, fallback, children}) {
- for await ({timeout, fallback, children} of this) {
+async function *Suspense({timeout, fallback, children}, ctx) {
+ for await ({timeout, fallback, children} of ctx) {
yield
{fallback} ;
yield
{children} ;
}
diff --git a/website/documents/guides/06-special-props-and-tags.md b/website/documents/guides/06-special-props-and-tags.md
index 05823e65..459b5263 100644
--- a/website/documents/guides/06-special-props-and-tags.md
+++ b/website/documents/guides/06-special-props-and-tags.md
@@ -5,10 +5,10 @@ title: Special Props and Tags
Crank provides certain APIs in the form of special props or element tags. The following is an overview of these props and tags.
## Special Props
-The following props apply to all elements, regardless of tag or renderer.
+The following prop names have special behavior. They are not passed to host elements and should not be used to define component props.
-### crank-key
-By default, Crank uses an element’s tag and position to determine if it represents an update or a change to the tree. Because elements often represent stateful DOM nodes or components, it can be useful to *key* the children of an element to hint to the renderer that an element has been added, moved or removed from a parent. In Crank, we do this with the special prop `crank-key`:
+### key
+By default, Crank uses an element’s tag and position to determine if it represents an update or a change to the tree. Because elements often represent stateful DOM nodes or components, it can be useful to *key* the children of an element to hint to the renderer that an element has been added, moved or removed from a parent. In Crank, we do this with the special prop `key`:
```jsx live
import {createElement} from "https://unpkg.com/@b9g/crank/crank";
@@ -28,23 +28,24 @@ function *List() {
reversed = !reversed;
this.refresh();
};
+
for ({} of this) {
yield (
{
reversed ? (
<>
-
-
-
-
+
+
+
+
>
) : (
<>
-
-
-
-
+
+
+
+
>
)
}
@@ -57,7 +58,7 @@ function *List() {
renderer.render(
, document.body);
```
-Keys are scoped to an element’s children, and can be any JavaScript value. When rendering iterables, it’s useful to key elements of the iterable, because it’s common for the values of rendered iterables to added, moved or removed.
+All elements in the element tree can be keyed. They are scoped to siblings, and can be any JavaScript value. The most common use-case is when rendering iterables, as the iterable can be rearranged.
```jsx live
import {createElement} from "https://unpkg.com/@b9g/crank/crank";
@@ -65,8 +66,8 @@ import {renderer} from "https://unpkg.com/@b9g/crank/dom";
function *Shuffler() {
let nextId = 0;
- const els = Array.from({length: 4}, (_, i) =>
{i} );
- while (true) {
+ const els = Array.from({length: 4}, (_, i) =>
{i} );
+ for ({} of this) {
yield
{els}
;
els.reverse();
}
@@ -90,14 +91,11 @@ console.log(document.body.innerHTML);
console.log(document.firstChild.firstChild === span); // true
```
-All elements in the element tree can be keyed. If the element is a component element, the `crank-key` prop is erased from the props object passed to the component.
-
-### crank-ref
-Sometimes, you may want to access the rendered value of a specific element in the element tree. To do this, you can pass a callback as the `crank-ref` prop. This callback is called with the rendered value of the element when the element has committed.
+### ref
+Sometimes, you may want to access the rendered value of a specific element in the element tree. To do this, you can pass a callback as the `ref` prop. This callback is called with the rendered value of the element when the element has committed.
```jsx live
-import {createElement} from "https://unpkg.com/@b9g/crank/crank";
-import {renderer} from "https://unpkg.com/@b9g/crank/dom";
+import {renderer} from "@b9g/crank";
function *MyPlayer() {
let audio;
@@ -108,7 +106,7 @@ function *MyPlayer() {
(audio = el)}
+ ref={(el) => (audio = el)}
/>
);
@@ -118,14 +116,62 @@ function *MyPlayer() {
renderer.render(
, document.body);
```
-Refs can be attached to any element in the element tree, and the value passed to the callback will vary according the type of the element and the specific renderer.
+Ref callbacks fire once the first time a host element is rendered. They do not work on fragment elements. For component elements, the `ref` prop must be explicitly passed to a component's child. This is useful when writing elements which wrap a host element.
+
+```jsx
+function MyInput({ref, class, ...props}) {
+ return
+}
+```
+
+### copy
+
+The `copy` prop is used to prevent the re-rendering of any element and its children. A truthy value indicates that the element should not re-render. It can be used to prevent rendering, or for performance reasons.
+
+```jsx
+function* List({elements}) {
+ for ({elements} of this) {
+ yield (
+
+ {elements.map((el) => {
+ // The copy prop will prevent non-initial renders from updating the DOM.
+ return (
+
+ {el.value}
+
+ );;
+ })}
+
+ );
+ }
+}
+```
### children
-The `children` prop passed to components is special because it is not usually set with JSX’s `key="value"` prop syntax, but by the contents between the opening and closing tags. Crank places no limitations on the types of values that can be passed into components as children, but patterns like [render props](https://reactjs.org/docs/render-props.html) from the React community, where a callback is passed as the child of a component, should be avoided.
+The `children` prop passed to components is special because it is not usually set with JSX’s `key="value"` prop syntax, but by the contents between the opening and closing tags. It is the responsibility of the component to make sure the `children` passed in are rendered in its yielded or returned element tree.
-The actual type of the `children` prop will vary according to the number of children passed in. If a component element has no children (`
`), the `children` prop will be undefined, if it has one child (`
`), the `children` prop will be set to that child, and if it has multiple children (`
`), the `children` prop will be set to an array of those children. We do this to reduce runtime memory costs. All props have to be retained between renders, and most elements contain only zero or one child, so avoiding the allocation of an extra array for every element in the tree can noticeably reduce memory requirements.
+```jsx
+function Component({children}) {
+ console.log(children);
+ return (
+
{children}
+ );
+}
+
+renderer.render(
Hello world , document.body);
+// logs "Hello world"
+
+renderer.render(
+
+ 1
+ 2
+ 3
+ ,
+ document.body,
+);
+// logs an array of virtual elements representing the child divs.
+```
-Therefore, the `children` prop should be treated as a black box, only to be rendered somewhere within a component’s returned or yielded children. Attempting to iterate over or manipulate the passed in children of a component is an anti-pattern, and you should use [event dispatch](./handling-events#dispatching-events) or [provisions](./reusable-logic#provisions) to coordinate ancestor and descendant components.
## Special DOM Props
@@ -195,7 +241,7 @@ function equals(props, newProps) {
}
function memo(Component) {
- return function *Wrapped({props}) {
+ return function *Wrapped(props) {
yield
;
for (const newProps of this) {
if (equals(props, newProps)) {
@@ -210,7 +256,7 @@ function memo(Component) {
}
```
-In this example, `memo` is a higher-order component, a function which takes a component and returns a component. This wrapper component compares old and new props and yields a `Copy` element if every prop is shallowly equal. A `Copy` element can appear anywhere in an element tree to prevent rerenderings, and the only props `Copy` elements take are the `crank-key` and `crank-ref` props, which work as expected.
+In this example, `memo` is a higher-order component, a function which takes a component and returns a component. This wrapper component compares old and new props and yields a `Copy` element if every prop is shallowly equal. A `Copy` element can appear anywhere in an element tree to prevent rerenderings, and the only props `Copy` elements take are the `key` and `ref` props, which work as expected.
### Portal
Sometimes you may want to render into a DOM node which isn’t the current parent element, or even a part of the currently rendered DOM tree. You can do this with the `Portal` tag, passing in a DOM node as its `root` prop. The Portal’s children will be rendered into the specified root element, just as if Renderer.render was called with the root value as its second argument.
@@ -258,4 +304,4 @@ function MarkdownViewer({markdown=""}) {
}
```
-Be careful when using `Raw` elements, as passing unsanitized text inputs can lead to security vulnerabilities.
+Be careful when using `
` elements, as passing unsanitized text inputs can lead to security vulnerabilities.
diff --git a/website/documents/guides/07-lifecycles.md b/website/documents/guides/07-lifecycles.md
index 7d70dd1c..849c93be 100644
--- a/website/documents/guides/07-lifecycles.md
+++ b/website/documents/guides/07-lifecycles.md
@@ -2,178 +2,232 @@
title: Lifecycles
---
-Crank uses generator functions rather than hooks or classes to define component lifecycles. Internally, this is achieved by calling the `next`, `return` and `throw` methods of the returned generator object as components are mounted, updated and unmounted from the element tree. As a developer, you can use the `yield`, `return`, `try`, `catch`, and `finally` keywords within your generator components to take full advantage of the generator’s natural lifecycle.
+Crank uses generator functions to define component lifecycles. Internally, this is achieved by calling the [`next()`, `return()` and `throw()` methods of generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator#instance_methods) returned from components. As a developer, this means you can use standard JavaScript control flow to execute code during the lifecycle of a component. For parts of the lifecycle which cannot be placed in the generator body itself, Crank provides the lifecycle methods `schedule()`, `flush()` and `cleanup()` on the context.
+
+## Setup, update and teardown logic
+
+The execution of Crank components is well-defined and well-behaved, so there are no restrictions around where you need to place side-effects. This means much of setup, update and teardown logic can be placed directly in components.
+
+```jsx live
+import {renderer} from "@b9g/crank/dom";
+
+function *Blinker({seconds}) {
+ // setup logic can go at the top of the scope
+ let blinking = false;
+ const blink = async () => {
+ blinking = true;
+ this.refresh();
+ await new Promise((r) => setTimeout(r, 100));
+ blinking = false;
+ this.refresh();
+ };
+
+ let interval = setInterval(blink, seconds * 1000);
+ let oldSeconds = seconds;
+
+ for ({seconds} of this) {
+ // update logic can go directly in the loop
+ if (seconds !== oldSeconds) {
+ blinking = false;
+ clearInterval(interval);
+ interval = setInterval(blink, seconds * 1000);
+ oldSeconds = seconds;
+ }
-## Returning Values
+ console.log(blinking);
-In most generator components, you will yield children within a loop so that they can continue to respond to updates. However, you may also want to return a final state. Unlike function components, which are called and returned once for each update, once a generator component returns, its rendered value is final, and the component will never update again.
+ yield (
+
+ {blinking && "!!!"}
+
+ );
+ }
-```jsx
-function *Stuck({message}) {
- return {message}
;
+ // cleanup logic can go at the end of the loop
+ clearInterval(interval);
}
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "Hello
"
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "Hello
"
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "Hello
"
+function *App() {
+ let seconds = 1;
+ const onChange = (ev) => {
+ seconds = ev.target.value;
+ this.refresh();
+ };
+
+ for ({} of this) {
+ yield (
+
+ Seconds: {" "}
+
+
+
+ );
+ }
+}
+
+renderer.render( , document.body);
```
-You should be careful when writing generator components to make sure that you always place your `yield` operators in a `for` or `while` loop. If you forget and implicitly return from the generator, it will stop updating and nothing will be rendered ever again.
+## Working with the DOM
+
+Logic which needs to happen after rendering, such as doing direct DOM manipulations or taking measurements, can be done directly after a `yield` in async generator components whic use `for await...of` loops, because the component is continuously resumed until the bottom of the `for await` loop. Conveniently, the `yield` expression will evaluate to the rendered result of the component.
```jsx
-function *Numbers() {
- yield 1;
- yield 2;
- yield 3;
+async function *Component(this, props) {
+ for await (props of this) {
+ const div = yield
;
+ // logic which manipulates the div can go here.
+ div.innerHTML = props.innerHTML;
+ }
}
-
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "1"
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "2"
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "3"
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // ""
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // ""
```
-## Cleaning Up
-
-When a generator component is removed from the tree, Crank calls the `return` method on the component’s generator object. You can think of it as whatever `yield` expression your component was suspended on being replaced by a `return` statement. This means any loops your component was in when the generator suspended are broken out of, and code after the yield does not execute.
-
-You can take advantage of this behavior by wrapping your `yield` loops in a `try`/`finally` block to release any resources that your component may have used.
+Unfortunately, this approach will not work for code in `for...of` loops. In a `for...of` loop, the behavior of `yield` works such that the component will suspend at the `yield` for each render, and this behavior holds for both sync and async generator components. This is necessary for sync generator components, because there is nowhere else to suspend, and is mimicked in async generator components, to make refactoring between sync and async generator components easier.
```jsx
-function *Cleanup() {
- try {
- while (true) {
- yield "Hi";
- }
- } finally {
- console.log("finally block executed");
+// The following behavior happens in both sync and async generator components
+// so long as they use a `for...of` and not a `for await...of` loop.
+
+function *Component(this, props) {
+ let div = null;
+
+ const onclick = () => {
+ // If the component is only rendered once, div will still be null.
+ console.log(div);
+ };
+ for ({} of this) {
+ // This does not work in sync components because the function is paused
+ // exactly at the yield. Only after rendering a second time will cause the
+ // div variable to be assigned.
+ div = yield Click me ;
+ // Any code below the yield will not run until the next render.
}
}
-
-renderer.render( , document.body);
-console.log(document.body); // "Hi"
-renderer.render(null, document.body);
-// "finally block executed"
-console.log(document.body); // ""
```
-[The same best practices](https://eslint.org/docs/rules/no-unsafe-finally) which apply to `try`/`finally` statements in regular functions apply to generator components. In short, you should not yield or return anything in the `finally` block. Crank will not use the yielded or returned values and doing so might cause your components to inadvertently swallow errors or suspend in unexpected locations.
+Thankfully, the Crank context provides two callback-based methods which allow you to run code after rendering has completed: `schedule()` and `flush()`.
-## Catching Errors
-We all make mistakes, and it can be useful to catch errors thrown by our components so that we can show the user something or notify error-logging services. To facilitate this, Crank will catch errors thrown when rendering child elements and throw them back into parent generator components using the `throw` method on the component’s generator object. You can think of it as whatever `yield` expression your component was suspended on being replaced with a `throw` statement with the error set to whatever was thrown by the component’s children.
+The `schedule()` method behaves like code which runs in an async generator’s `for await...of` loop. It runs immediately after the children DOM nodes are created:
-You can take advantage of this behavior by wrapping your `yield` operations in a `try`/`catch` block to catch errors caused by children.
-
```jsx
-function Thrower() {
- throw new Error("Hmmm");
-}
-
-function *Catcher() {
- try {
- yield ;
- } catch (err) {
- return Error: {err.message}
;
+function *Component(this, props) {
+ for await (props of this) {
+ this.schedule((div) => {
+ // the div is
+ div.innerHTML = props.innerHTML;
+ });
+ yield
;
}
}
-
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "Error: Hmmm
"
-renderer.render( , document.body);
-renderer.render( , document.body);
-renderer.render( , document.body);
-console.log(document.body.innerHTML); // "Error: Hmmm
"
```
-As explained previously, this component “sticks” because it uses a return statement, so that the same error message is shown until the component is unmounted. However, you may also want to recover from errors as well, and you can do this by ignoring or handling the error.
+On the other hand, the `flush()` method runs after the result is completely rendered and live in the DOM. This is required for DOM methods like the `focus()` method for auto-focusing after render. The reason for the distinction between `schedule()` and `flush()` is that Crank allows rendering to be async, and coordinates async rendering so that the rendering of multiple async siblings happens together, meaning there might be some time before a created DOM node is added to its intended parent.
-```jsx
-function T1000() {
- throw new Error("Die!!!");
+
+```jsx live
+import {renderer} from "@b9g/crank/dom";
+function *AutoFocusingInput(props) {
+ // this.schedule does not work because it fires before the input element is
+ // added to the DOM
+ // this.schedule((input) => input.focus());
+ this.flush((input) => input.focus());
+ for (props of this) {
+ yield ;
+ }
}
-function *Terminator() {
- while (true) {
- yield Come with me if you want to live
;
- try {
- yield ;
- } catch (err) {
- yield I’ll be back
;
- }
+function *Component() {
+ let initial = true;
+ for ({} of this) {
+ yield (
+
+
+
+ this.refresh()}>Refresh
+
+
+ );
+
+ initial = false;
}
}
-renderer.render( , document.body);
-console.log(document.body.innerHTML);
-// "Come with me if you want to live
"
-renderer.render( , document.body);
-console.log(document.body.innerHTML);
-// "I’ll be back
"
-renderer.render( , document.body);
-console.log(document.body.innerHTML);
-// "Come with me if you want to live
"
-renderer.render( , document.body);
-console.log(document.body.innerHTML);
-// "I’ll be back
"
+renderer.render( , document.body);
```
-## Accessing Rendered Values
-Sometimes, declarative rendering is not enough, and you’ll want to access the actual DOM nodes you’ve rendered, to make measurements or call imperative methods like the `focus` method for form elements, or the `play` method for media elements. To facilitate this, Crank will pass rendered DOM nodes back into the generator using the `next` method. This means that as a developer, you can read `yield` expressions to access the actual rendered DOM nodes.
+## Catching Errors
-```jsx
-async function *FocusingInput(props) {
- for await (props of this) {
- const input = yield ;
- input.focus();
+It can be useful to catch errors thrown by components to show the user an error notification or to notify error-logging services. To facilitate this, Crank will cause `yield` expressions to rethrow errors which happen when rendering children. You can take advantage of this behavior by wrapping your `yield` operations in a `try`/`catch` block to catch errors caused by children.
+
+```jsx live
+import {renderer} from "@b9g/crank/dom";
+function Thrower() {
+ if (Math.random() > 0.5) {
+ throw new Error("Oops");
}
-}
-```
-The `FocusingInput` component focuses every time it is rerendered. We use an async generator component here because async generators continuously resume, so the `input.focus` call happens directly after the component is rendered. While we also pass rendered nodes into sync generator components as well, attempting to access them directly after the `yield` may lead to surprising results.
+ return No errors
;
+}
-```jsx
-function *FocusingInput(props) {
- for (props of this) {
- const input = yield ;
- // This line does not execute until the component is rerendered.
- input.focus();
+function *Catcher() {
+ for ({} of this) {
+ try {
+ yield (
+
+
+ this.refresh()}>Rerender
+
+ );
+ } catch (err) {
+ yield (
+
+
Error: {err.message}
+
this.refresh()}>Retry
+
+ );
+ }
}
}
-```
-The problem is that sync generator components suspend at the point of yield expressions and only resume when updated by the parent or by a call to the `refresh` method. This means that if you were to try to access the rendered value via a `yield` expression, your code would not execute until the moment the component rerenders. You can imagine this as the generator function above suspended exactly before the `yield` expression is assigned to the `input` variable.
+renderer.render( , document.body);
+```
-To solve this problem, Crank provides an additional method on the context called `schedule`, which takes a callback and calls it with the rendered value after the component executes.
+## Additional cleanup methods
-```jsx
-function *FocusingInput(props) {
- for (props of this) {
- this.schedule((input) => input.focus());
- yield ;
- }
-}
-```
+When a generator component is removed from the tree, Crank calls the `return` method on the component’s generator object. You can think of it as whatever `yield` expression your component was suspended on being replaced by a `return` statement. This means any loops your component was in when the generator suspended are broken out of, and code after the yield does not execute.
-The `schedule` method fires the passed in callback synchronously when component finally renders. However, one unfortunate consequence of using a callback is that we lose the sequential execution of code which makes generator components so elegant and easy to understand. We can recover some of this linearity by using the `schedule` method with the `refresh` method.
+You can take advantage of this behavior by wrapping your `yield` loops in a `try`/`finally` block to release any resources that your component may have used.
```jsx
-function *FocusingInput(props) {
- this.schedule(() => this.refresh());
- const input = yield ;
- for (props of this) {
- input.focus();
- yield ;
+import {renderer} from "@b9g/crank/dom";
+
+function *Cleanup() {
+ try {
+ while (true) {
+ yield "Hi";
+ }
+ } finally {
+ console.log("finally block executed");
}
}
+
+renderer.render( , document.body);
+console.log(document.body); // "Hi"
+renderer.render(null, document.body);
+// "finally block executed"
+console.log(document.body); // ""
```
-The focusing input now focuses before the children are yielded, but because the same input is yielded, the result is mostly the same. The `schedule` method is designed to work with the `refresh` method so that sync generator components can schedule multiple rendering passes which work synchronously.
+[The same best practices](https://eslint.org/docs/rules/no-unsafe-finally) which apply to `try`/`finally` statements in regular functions apply to generator components. In short, you should not yield or return anything in the `finally` block. Crank will not use the yielded or returned values and doing so might cause your components to inadvertently swallow errors or suspend in unexpected locations.
+
+## Returning Values
diff --git a/website/documents/index.md b/website/documents/index.md
index f2d1b4d0..47e6b9e4 100644
--- a/website/documents/index.md
+++ b/website/documents/index.md
@@ -5,27 +5,25 @@ description: "The Just JavaScript framework. Crank is a JavaScript / TypeScript
## What is Crank?
-Crank is a JavaScript / TypeScript library for building websites and applications. It is a framework where components are defined with plain old functions, including async and generator functions, which `yield` and `return` JSX.
+Crank is a JavaScript / TypeScript library for building websites and applications. It is a framework where components are defined with plain old functions, including async and generator functions, which `yield` and `return` JSX elements.
## Why is Crank “Just JavaScript?”
-Many web frameworks claim to be “just JavaScript.”
-
-Few have as strong a claim as Crank.
+Many web frameworks claim to be “just JavaScript.” Few have as strong a claim as Crank.
It starts with the idea that you can write components with *all* of JavaScript’s built-in function syntaxes.
```jsx live
import {renderer} from "@b9g/crank/dom";
-function *Timer() {
+function *Timer({}, ctx) {
let seconds = 0;
const interval = setInterval(() => {
seconds++;
- this.refresh();
+ ctx.refresh();
}, 1000);
- for ({} of this) {
+ for ({} of ctx) {
yield {seconds} second{seconds !== 1 && "s"}
;
}
@@ -69,7 +67,7 @@ function Greeting({name = "World"}) {
return Hello {name}.
;
}
-function RandomName() {
+function RandomName({}, ctx) {
const names = ["Alice", "Bob", "Carol", "Dave"];
const randomName = names[Math.floor(Math.random() * names.length)];
@@ -78,7 +76,7 @@ function RandomName() {
{/*
- this.refresh()}>Random name
+ ctx.refresh()}>Random name
*/}
);
@@ -182,14 +180,14 @@ function Greeting({name = "World"}) {
return Hello {name}.
;
}
-function *CyclingName() {
+function *CyclingName({}, ctx) {
const names = ["Alice", "Bob", "Carol", "Dave"];
let i = 0;
while (true) {
yield (
- this.refresh()}>Cycle name
+ ctx.refresh()}>Cycle name
)
@@ -207,13 +205,13 @@ Never memoize a callback ever again.
```jsx live
import {renderer} from "@b9g/crank/dom";
-function *Timer() {
+function *Timer({}, ctx) {
let interval = null;
let seconds = 0;
const startInterval = () => {
interval = setInterval(() => {
seconds++;
- this.refresh();
+ ctx.refresh();
}, 1000);
};
@@ -225,18 +223,18 @@ function *Timer() {
interval = null;
}
- this.refresh();
+ ctx.refresh();
};
const resetInterval = () => {
seconds = 0;
clearInterval(interval);
interval = null;
- this.refresh();
+ ctx.refresh();
};
// The this of a Crank component is an iterable of props.
- for ({} of this) {
+ for ({} of ctx) {
// Welcome to the render loop.
// Most generator components should use render loops even if they do not
// use props.
@@ -254,7 +252,7 @@ function *Timer() {
);
}
- // You can even put cleanup code after the loop.
+ // You can place cleanup code after the loop.
clearInterval(interval);
}
@@ -289,7 +287,7 @@ async function Definition({word}) {
);
}
-function *Dictionary() {
+function *Dictionary({}, ctx) {
let word = "";
const onsubmit = (ev) => {
ev.preventDefault();
@@ -297,11 +295,11 @@ function *Dictionary() {
const word1 = formData.get("word");
if (word1.trim()) {
word = word1;
- this.refresh();
+ ctx.refresh();
}
};
- for ({} of this) {
+ for ({} of ctx) {
yield (
<>
, document.body);
```
-Async generator functions let you write components that are both async *and* stateful. Crank uses promises wherever it makes sense, and has a rich async execution model which allows you to do things like racing components to display loading states.
+Async generator functions let you write components that are both async *and* stateful. Crank uses promises wherever they makes sense, and has a rich async execution model which allows you to do things like racing components to display loading states.
```jsx live
import {renderer} from "@b9g/crank/dom";
@@ -366,17 +364,17 @@ function CreditCard({type, expiration, number, owner}) {
);
}
-async function *LoadingCreditCard() {
+async function *LoadingCreditCard({}, ctx) {
await new Promise((r) => setTimeout(r, 1000));
let count = 0;
const interval = setInterval(() => {
count++;
- this.refresh();
+ ctx.refresh();
}, 200);
- this.cleanup(() => clearInterval(interval));
+ ctx.cleanup(() => clearInterval(interval));
- for ({} of this) {
+ for ({} of ctx) {
yield (
this.refresh());
+async function *RandomCreditCard({throttle}, ctx) {
+ setTimeout(() => ctx.refresh());
yield null;
- for await ({throttle} of this) {
+ for await ({throttle} of ctx) {
yield ;
yield ;
}
}
-function *CreditCardGenerator() {
+function *CreditCardGenerator({}, ctx) {
let throttle = false;
const toggleThrottle = () => {
throttle = !throttle;
// TODO: A nicer user behavior would be to not generate a new card
// when toggling the throttle.
- this.refresh();
+ ctx.refresh();
};
- for ({} of this) {
+ for ({} of ctx) {
yield (
- this.refresh()}>
+ ctx.refresh()}>
Generate new card
{" "}