Skip to content

Commit

Permalink
update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Kim committed Feb 29, 2024
1 parent cbec13b commit e76614d
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 237 deletions.
4 changes: 1 addition & 3 deletions website/documents/guides/01-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,4 @@ export default defineConfig({
});
```

## Shovel

A full-stack framework is in the works for Crank. Stay tuned.
## Key Examples
5 changes: 1 addition & 4 deletions website/documents/guides/02-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ title: Elements and Renderers
Crank works with [JSX](https://facebook.github.io/jsx/), a well-supported, XML-like syntax extension to JavaScript.

### Two types of JSX transpilation
Historically speaking, there are two ways to transform JSX: the *classic* and *automatic* transforms. Crank supports both formats.
There are two ways to transform JSX: the *classic* and *automatic* transforms. Crank supports both formats.

The classic transform turns JSX elements into `createElement()` calls.

Expand Down Expand Up @@ -83,7 +83,6 @@ console.log(html); // <div id="element">Hello world</div>

## The Parts of an Element

<!-- TODO: Make this a JSX element -->
![Image of a JSX element](/static/parts-of-jsx.svg)

An element can be thought of as having three main parts: a *tag*, *props* and *children*. These roughly correspond to the syntax for HTML, and for the most part, you can copy-paste HTML into JSX-flavored JavaScript and have things work as you would expect. The main difference is that JSX has to be well-balanced like XML, so void tags must have a closing slash (`<hr />` not `<hr>`). Also, if you forget to close an element or mismatch opening and closing tags, the parser will throw an error, whereas HTML can be unbalanced or malformed and mostly still work.
Expand Down Expand Up @@ -198,5 +197,3 @@ renderer.render(
console.log(document.body.firstChild === div); // true
console.log(document.body.firstChild.firstChild === span); // true
```

**Note:** The documentation tries to avoid the terms “virtual DOM” or “DOM diffing” insofar as the core renderer can be extended to target multiple environments; instead, we use the terms “virtual elements” and “element diffing” to mean mostly the same thing.
66 changes: 46 additions & 20 deletions website/documents/guides/03-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
title: Components
---

So far, we’ve only seen and used *host elements*, lower-case elements like `<a>` or `<div>`, which correspond to HTML. Eventually, we’ll want to group these elements into reusable *components*. Crank uses plain old JavaScript functions to define components. The type of the function determines the component’s behavior.

## Basic Components

So far, we’ve only seen and used *host elements*. By convention, all host elements use lowercase tags like `<a>` or `<div>`, and these elements are rendered as their HTML equivalents.

However, eventually we’ll want to group these elements into reusable *components*. In Crank, components are defined with plain old JavaScript functions, including async and generator functions, which return or yield JSX elements. These functions can be referenced as element tags, and component elements are distinguished from host elements through the use of capitalized identifiers. The capitalized identifier is not just a convention but a way to tell JSX compilers to interpret the tag as an identifier rather than a literal string.

The simplest kind of component is a *function component*. When rendered, the function is invoked with the props of the element as its first argument, and the return value of the function is rendered as the element’s children.

```jsx live
Expand Down Expand Up @@ -73,8 +76,8 @@ In the preceding example, the component’s local state was updated directly whe

Crank allows components to control their own execution by passing in an object called a *context* as the `this` keyword of each component. Contexts provide several utility methods, the most important of which is the `refresh()` method, which tells Crank to update the related component instance in place.

```jsx
function *Timer() {
```jsx live
function *Timer({message}) {
let seconds = 0;
const interval = setInterval(() => {
seconds++;
Expand All @@ -84,26 +87,50 @@ function *Timer() {
try {
while (true) {
yield (
<div>Seconds elapsed: {seconds}</div>
<div>{message} {seconds}</div>
);
}
} finally {
clearInterval(interval);
}
}

renderer.render(<Timer message="Seconds elapsed:" />, document.body);
```

This `<Timer />` component is similar to the `<Counter />` one, except now the state (the local variable `seconds`) is updated in a `setInterval()` callback, rather than when the component is rerendered. Additionally, the `refresh()` method is called to ensure that the generator is stepped through whenever the `setInterval()` callback fires, so that the rendered DOM actually reflects the updated `seconds` variable.
This `<Timer>` component is similar to the `<Counter>` one, except now the state (the local variable `seconds`) is updated in a `setInterval()` callback, rather than when the component is rerendered. Additionally, the `refresh()` method is called to ensure that the generator is stepped through whenever the `setInterval()` callback fires, so that the rendered DOM actually reflects the updated `seconds` variable. Finally, the `<Timer>` component is passed a display message as a prop.

One important detail about the `Timer` example is that it cleans up after itself with `clearInterval()` in the `finally` block. Behind the scenes, Crank will call the `return()` method on an element’s generator object when it is unmounted.

If you hate the idea of using the `this` keyword, the context is also passed in as the second parameter of components.

```jsx
function *Timer({message}, ctx) {
let seconds = 0;
const interval = setInterval(() => {
seconds++;
ctx.refresh();
}, 1000);

try {
while (true) {
yield (
<div>{message} {seconds}</div>
);
}
} finally {
clearInterval(interval);
}
}
```

## The Render Loop

The generator components we’ve seen so far haven’t used props. They’ve also used while (true) loops, which was done mainly for learning purposes. In actuality, Crank contexts are iterables of props, so you can `for...of` iterate through them.
The `<Timer>` component works, but it can be improved. Firstly, while the component is stateful, it would not update the message if it was rerendered with new props. Secondly, the `while (true)` loop can iterate infinitely if you forget to add a `yield`. To solve these issues, Crank contexts are an iterable of the latest props.

```jsx live
import {renderer} from "@b9g/crank/dom";
function *Timer({message}) {
function *Timer(this, {message}) {
let seconds = 0;
const interval = setInterval(() => {
seconds++;
Expand All @@ -120,27 +147,25 @@ function *Timer({message}) {
}

renderer.render(
<Timer message="Seconds elapsed" />,
<Timer message="Seconds elapsed:" />,
document.body,
);

setTimeout(() => {
renderer.render(
<Timer message="Hello from the timeout" />,
<Timer message="Seconds elapsed (updated in setTimeout):" />,
document.body,
);
}, 4500);
}, 2500);
```

The loop created by iterating over contexts is called the *render loop*. By replacing the `while` loop with a `for...of` loop which iterates over `this`, you can get the latest props each time the generator is resumed.
The loop created by iterating over contexts is called the *render loop*. By replacing the `while` loop with a `for...of` loop, you can get the latest props each time the generator is resumed. It also provides benefits over `while` loops, like throwing errors if you forget to `yield`, and allowing you to write cleanup code after the loop without having to wrap the block in a `try`/`finally` block.

The render loop has additional advantages over while loops. For instance, you can place cleanup code directly after the loop. The render loop will also throw errors if it has been iterated without a yield, to prevent infinite loops.

One Crank idiom you may have noticed is that we define props in component parameters, and overwrite them using a destructuring expression in the `for...of` statement. This is an easy way to make sure those variables stay in sync with the current props of the component. For this reason, even if your component has no props, it is idiomatic to use a render loop.
One Crank idiom you may have noticed is that we define props in function parameters, and overwrite them using a destructuring expression in the `for...of` statement. This is an easy way to make sure those variables stay in sync with the current props of the component. For this reason, even if your component has no props, it is idiomatic to destructure props and use a `for...of` loop.

```jsx live
import {renderer} from "@b9g/crank/dom";
function *Counter() {
function *Counter(this, {}) {
let count = 0;
const onclick = () => {
count++;
Expand All @@ -160,17 +185,18 @@ renderer.render(<Counter />, document.body);
```

## Default Props
You may have noticed in the preceding examples that we used [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring) on the props parameter for convenience. You can further assign default values to specific props using JavaScript’s default value syntax.
Because we use [object destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring), you can further assign default values to specific props using JavaScript’s default value syntax.

```jsx
```jsx live
import {renderer} from "@b9g/crank/dom";
function Greeting({name="World"}) {
return <div>Hello, {name}</div>;
}

renderer.render(<Greeting />, document.body); // "<div>Hello World</div>"
renderer.render(<Greeting />, document.body);
```

This syntax works well for function components, but for generator components, you should make sure that you use the same default value in both the parameter list and the loop. A mismatch in the default values for a prop between these two positions may cause surprising behavior.
For generator components, you should make sure that you use the same default value in both the parameter list and the loop. A mismatch in the default values for a prop between these two positions may cause surprising behavior.

```jsx live
import {renderer} from "@b9g/crank/dom";
Expand Down
24 changes: 12 additions & 12 deletions website/documents/guides/04-handling-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Handling Events
Most web applications require some measure of interactivity, where the user interface updates according to input. To facilitate this, Crank provides several ways to listen to and trigger events.

## DOM Event Props
You can attach event callbacks to host element directly using event props. These props start with `on`, are all lowercase, and correspond to the event type (`onclick`, `onkeydown`). By combining event props, local variables and `this.refresh()`, you can write interactive components.
You can attach event callbacks to host element directly using event props. These props start with `on`, are by convention lowercase, and correspond to the event type (`onclick`, `onkeydown`). By combining event props, local variables and `this.refresh()`, you can write interactive components.

```jsx live
import {renderer} from "@b9g/crank/dom";
Expand Down Expand Up @@ -83,13 +83,6 @@ function *Counter() {
renderer.render(<Counter />, document.body);
```

## Event props vs EventTarget
The props-based event API and the context-based EventTarget API both have their advantages. On the one hand, using event props means you can listen to exactly the element you’d like to listen to.

On the other hand, using the `addEventListener` method allows you to take full advantage of the EventTarget API, which includes registering passive event listeners, or listeners which are dispatched during the capture phase. Additionally, the EventTarget API can be used without referencing or accessing the child elements which a component renders, meaning you can use it to listen to elements nested in other components.

Crank supports both API styles for convenience and flexibility.

## Dispatching Events
Crank contexts implement the full EventTarget interface, meaning you can use [the `dispatchEvent` method](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) and [the `CustomEvent` class](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) to dispatch custom events to ancestor components:

Expand All @@ -110,7 +103,7 @@ function MyButton(props) {

function MyButtons() {
return [1, 2, 3, 4, 5].map((i) => (
<p>
<p>
<MyButton id={"button" + i}>Button {i}</MyButton>
</p>
));
Expand Down Expand Up @@ -179,7 +172,14 @@ function *CustomCounter() {
renderer.render(<CustomCounter />, document.body);
```

Using custom events and event bubbling allows you to encapsulate state transitions within component hierarchies without the need for complex state management solutions used in other frameworks like Redux or VueX.
Using custom events and event bubbling allows you to encapsulate state transitions within component hierarchies without the need for complex state management solutions in a way that is DOM-compatible.

## Event props vs EventTarget
The props-based event API and the context-based EventTarget API both have their advantages. On the one hand, using event props means you can listen to exactly the element you’d like to listen to.

On the other hand, using the `addEventListener` method allows you to take full advantage of the EventTarget API, which includes registering passive event listeners, or listeners which are dispatched during the capture phase. Additionally, the EventTarget API can be used without referencing or accessing the child elements which a component renders, meaning you can use it to listen to elements nested in other components.

Crank supports both API styles for convenience and flexibility.

## Form Elements

Expand Down Expand Up @@ -213,7 +213,7 @@ function *Form() {
renderer.render(<Form />, document.body);
```

If your component is updating for other reasons, you can use the special property `$static` to prevent the input element from updating.
If your component is updating for other reasons, you can use the special property `copy` to prevent the input element from updating.

```jsx live
import {renderer} from "@b9g/crank/dom";
Expand All @@ -237,7 +237,7 @@ function *Form() {
reset = false;
yield (
<form onsubmit={onsubmit}>
<input type="text" value="" $static={currentReset} />
<input type="text" value="" copy={currentReset} />
<p>
<button onclick={onreset}>Reset</button>
</p>
Expand Down
24 changes: 12 additions & 12 deletions website/documents/guides/05-async-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
title: Async Components
---

So far, every component we’ve seen has been a sync function or sync generator component. Crank processes element trees containing synchronous components instantly, ensuring that by the time `renderer.render()` or `this.refresh()` completes execution, rendering will have finished, and the DOM will have been updated.
So far, every component we’ve seen has been a sync function or sync generator component. Crank processes sync components immediately, ensuring that by the time `renderer.render()` or the `refresh()` method completes execution, rendering will have finished.

Nevertheless, a JavaScript component framework would not be complete without a way to work with promises. Luckily, Crank also allows any component to be async the same way you would make any function asynchronous, by adding an `async` before the `function` keyword. Both *async function* and *async generator components* are supported. This feature means you can `await` promises in the process of rendering in virtually any component.
Nevertheless, a JavaScript framework would not be complete without a way to work with promises. To this end, Crank allows any component to be async the same way you would make any function asynchronous, by adding an `async` before the `function` keyword. Both *async function* and *async generator components* are supported. This feature means you can `await` promises in the process of rendering in virtually any component.

```jsx live
import {renderer} from "@b9g/crank/dom";
Expand All @@ -25,7 +25,7 @@ async function Definition({word}) {
await renderer.render(<Definition word="framework" />, document.body);
```

When rendering is async, `renderer.render()` and `this.refresh()` will return promises which settle when rendering has finished.
When rendering is async, `renderer.render()` and the `refresh()` method will return promises which settle when rendering has finished.

### Concurrent Updates
The nature of declarative rendering means that async components can be rerendered while they are still pending. Therefore, Crank implements a couple rules to make concurrent updates predictable and performant:
Expand Down Expand Up @@ -83,7 +83,7 @@ renderer.render(<Dictionary />, document.body);

`AsyncLabeledCounter` is an async version of the `LabeledCounter` example introduced in [the section on props updates](./components#props-updates). This example demonstrates several key differences between sync and async generator components. Firstly, rather than using `while` or `for…of` loops as with sync generator components, we now use [a `for await…of` loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of). This is possible because contexts are not just an *iterable* of props, but also an *async iterable* of props as well.

Secondly, you’ll notice that the async generator yields multiple times per iteration over `this`, once to show a loading message and once to show the actual count. While it is possible for sync generators components to yield multiple times per iteration over `this`, it wouldn’t necessarily make sense to do so because generators suspend at each yield, and upon resuming a second time within the same loop, the props would be stale. In contrast, async generator components are continuously resumed. Rather than suspending at each yield, we rely on the `for awaitof` loop, which suspends at its end until the next update.
Secondly, you’ll notice that the async generator yields multiple times per iteration over contexts, once to show a loading message and once to show the actual count. While it is possible for sync generators components to yield multiple times per iteration over contexts, it wouldn’t necessarily make sense to do so because generators suspend at each yield, and upon resuming a second time within the same loop, the props would be stale. In contrast, async generator components are continuously resumed. Rather than suspending at each yield, we rely on the `for await...of` loop, which suspends at its end until the next update.

### Loading Indicators
The async components we’ve seen so far have been all or nothing, in the sense that Crank can’t show anything until all promises in the tree have fulfilled. This can be a problem when you have an async call which takes longer than expected. It would be nice if parts of the element tree could be shown without waiting, to create responsive user experiences.
Expand All @@ -110,23 +110,23 @@ async function RandomDog({throttle = false}) {
);
}

async function *RandomDogLoader({throttle}) {
for await ({throttle} of this) {
async function *RandomDogLoader({throttle}, ctx) {
for await ({throttle} of ctx) {
yield <LoadingIndicator />;
yield <RandomDog throttle={throttle} />;
}
}

function *RandomDogApp() {
function *RandomDogApp({}, ctx) {
let throttle = false;
this.addEventListener("click", (ev) => {
ctx.addEventListener("click", (ev) => {
if (ev.target.tagName === "BUTTON") {
throttle = !throttle;
this.refresh();
ctx.refresh();
}
});

while (true) {
for ({} of ctx) {
yield (
<Fragment>
<div>
Expand All @@ -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 timeout={timeout}>{fallback}</Fallback>;
yield <Fragment>{children}</Fragment>;
}
Expand Down
Loading

0 comments on commit e76614d

Please sign in to comment.