Skip to content

Commit

Permalink
tweak docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Kim authored and brainkim committed Apr 26, 2024
1 parent 22c9462 commit 7a9565d
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 138 deletions.
30 changes: 18 additions & 12 deletions website/documents/guides/01-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,18 +257,24 @@ renderer.render(<Timer />, document.body);

```jsx live
import {renderer} from "@b9g/crank/dom";
async function Definition({word}) {
// API courtesy https://dictionaryapi.dev
const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`);
const data = await res.json();
if (!Array.isArray(data)) {
return <p>No definition found for {word}</p>;
}

async function QuoteOfTheDay() {
const res = await fetch("https://favqs.com/api/qotd");
const {quote} = await res.json();
return (
<p>
“{quote.body}” – <a href={quote.url}>{quote.author}</a>
</p>
);
const {phonetic, meanings} = data[0];
const {partOfSpeech, definitions} = meanings[0];
const {definition} = definitions[0];
return <>
<p>{word} <code>{phonetic}</code></p>
<p><b>{partOfSpeech}.</b> {definition}</p>
</>;
}

renderer.render(<QuoteOfTheDay />, document.body);
await renderer.render(<Definition word="framework" />, document.body);
```

### A Loading Component
Expand Down Expand Up @@ -315,10 +321,10 @@ function *RandomDogApp() {
for ({} of this) {
yield (
<Fragment>
<div>
<button>Show me another dog.</button>
</div>
<RandomDogLoader throttle={throttle} />
<p>
<button>Show me another dog.</button>
</p>
</Fragment>
);
}
Expand Down
10 changes: 4 additions & 6 deletions website/documents/guides/02-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ import {createElement} from "@b9g/crank";
const el = createElement("div", {id: "element"}, "An element");
```

Identifiers like `createElement`, `Fragment` must be manually imported.

The automatic transform turns JSX elements into function calls from an automatically imported namespace.
With the classic transform, the `createElement` function must be manually imported. The automatic transform turns JSX elements into function calls from an automatically imported namespace.

```jsx
/** @jsxImportSource @b9g/crank */
Expand Down Expand Up @@ -62,11 +60,11 @@ const profile = _jsxs("div", {

```

The automatic transform has the benefit of not requiring manual imports. Beyond this fact, there is no difference between the two transforms, and the `_jsx()`/`_jsxs()` functions are wrappers around `createElement()`.
The automatic transform has the benefit of not requiring manual imports. Beyond this fact, there is no difference between the two transforms, and the provided `_jsx()`/`_jsxs()` functions are wrappers around `createElement()`.

## Renderers

Crank ships with two renderer subclasses for the web: one for managing DOM nodes in a front-end application, available through the module `@b9g/crank/dom`, and one for creating HTML strings, available through the module `@b9g/crank/html`. You can use these modules to render interactive user interfaces in the browser and HTML responses on the server.
Crank provides two renderer subclasses for the web: one for managing DOM nodes in a front-end application, available through the module `@b9g/crank/dom`, and one for creating HTML strings, available through the module `@b9g/crank/html`. You can use these modules to render interactive user interfaces in the browser and HTML responses on the server.

```jsx
import {renderer as DOMRenderer} from "@b9g/crank/dom";
Expand Down Expand Up @@ -174,7 +172,7 @@ console.log(document.body.innerHTML); // "<div>123 abc</div>"
```

## Element Diffing
Crank uses the same “virtual DOM” diffing algorithm made popular by React, where we compare elements by tag and position to reuse DOM nodes. This approach allows you to write declarative code which focuses on producing the right tree, while the framework does the dirty work of mutating the DOM efficiently.
Crank uses the same “virtual DOM” diffing algorithm made popular by React, where elements are compared by tag and position to reuse DOM nodes. This approach allows you to write declarative code which focuses on producing the right tree, while the framework does the dirty work of mutating the DOM efficiently.

```jsx
renderer.render(
Expand Down
21 changes: 12 additions & 9 deletions website/documents/guides/03-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: 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.
However, eventually we’ll want to group these elements into reusable *components*. In Crank, all components are defined with plain old JavaScript functions which produce 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.

Expand Down Expand Up @@ -42,7 +42,7 @@ renderer.render(
);
```

The type of children is unknown, i.e. it could be an array, an element, or whatever else the caller passes in.
The type of children is unknown, e.g. it could be an array, an element, or whatever else the caller passes in.

## Stateful Components
Eventually, you’ll want to write components with local state. In Crank, we use [generator functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) to do so. These types of components are referred to as *generator components*.
Expand All @@ -58,7 +58,7 @@ function *Counter() {
count++;
yield (
<div>
You have updated this component {count} time{count !== 1 && "s"}.
This component has updated {count} time{count !== 1 && "s"}.
</div>
);
}
Expand All @@ -72,11 +72,12 @@ renderer.render(<Counter />, document.body);
By yielding elements rather than returning them, components can be made stateful using variables in the generator’s local scope. Crank uses the same diffing algorithm which reuses DOM nodes to reuse generator objects, so there will only be one execution of a generator component for a given element in the tree.

## The Crank Context
In the preceding example, the component’s local state was updated directly when the generator was executed. This is of limited value insofar as what we usually want want is to update according to events or timers.
In the preceding example, the component’s local state `count` was updated directly when the generator was re-rendered. This is of limited value insofar as what we usually want want is to update according to events or timers.

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 live
import {renderer} from "@b9g/crank/dom";
function *Timer({message}) {
let seconds = 0;
const interval = setInterval(() => {
Expand All @@ -100,7 +101,7 @@ 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. 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.
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 the component’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.

Expand All @@ -126,7 +127,7 @@ function *Timer({message}, ctx) {

## The Render Loop

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.
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`, leading to unresponsive pages. To solve both of these issues, Crank contexts are themselves an iterable of props.

```jsx live
import {renderer} from "@b9g/crank/dom";
Expand All @@ -139,7 +140,7 @@ function *Timer({message}) {

for ({message} of this) {
yield (
<div>{message}: {seconds}</div>
<div>{message} {seconds}</div>
);
}

Expand All @@ -159,9 +160,9 @@ setTimeout(() => {
}, 2500);
```

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 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 prevents common development mistakes by throwing errors if you forget to yield, or yield multiple times in a loop. FInally, it also allows you to write cleanup code after the loop without having to wrap the entire loop in a `try`/`finally` block, as you would in a `while` 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.
One Crank idiom you may have noticed is that we define props in function parameters and overwrite them using a destructuring expression. 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";
Expand All @@ -172,6 +173,8 @@ function *Counter() {
this.refresh();
};

// using an empty destructuring expression means we do not need to declare
// more variables when there are no props
for ({} of this) {
yield (
<button onclick={onclick}>
Expand Down
24 changes: 10 additions & 14 deletions website/documents/guides/04-handling-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
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.
Most web applications require some measure of interactivity, where the user interface updates according to interactions like clicks and form inputs. 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 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.
You can attach event callbacks to host element directly using event props. These props start with `on`, are 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 All @@ -28,6 +28,8 @@ function *Counter() {
renderer.render(<Counter />, document.body);
```

Camel-cased event props are supported for compatibility reasons starting in 0.6.

## The EventTarget Interface
As an alternative to event props, Crank contexts implement the same `EventTarget` interface used by the DOM. The `addEventListener()` method attaches a listener to a component’s root DOM node.

Expand All @@ -52,11 +54,7 @@ function *Counter() {
renderer.render(<Counter />, document.body);
```

The context’s `addEventListener()` method attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation.

While the `removeEventListener()` method is implemented, you do not have to call the `removeEventListener()` method if you merely want to remove event listeners when the component is unmounted.

Because the event listener is attached to the outer `div`, we have to filter events by `ev.target.tagName` in the listener to make sure we’re not incrementing `count` based on clicks which don’t target the `button` element.
The context’s `addEventListener()` method attaches to the top-level node or nodes which each component renders, so if you want to listen to events on a nested node, you must use event delegation. For instance, in the following example, we have to filter events by target to make sure we’re not incrementing `count` based on clicks to the outer `div`.

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

While the `removeEventListener()` method is implemented, you do not have to call the `removeEventListener()` method if you merely want to remove event listeners when the component is unmounted.

## 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 Down Expand Up @@ -175,15 +175,11 @@ 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 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.
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. Crank supports both API styles for convenience and flexibility.

## Form Elements

Because Crank uses explicit state updates, it doesn’t need a concept like “controlled” `value` vs “uncontrolled” `defaultValue` props in React. No update means the value is uncontrolled.
Because Crank uses explicit state updates, it doesn’t require “controlled” or “uncontrolled” form props. No render means no update.

```jsx live
import {renderer} from "@b9g/crank/dom";
Expand Down Expand Up @@ -213,7 +209,7 @@ function *Form() {
renderer.render(<Form />, document.body);
```

If your component is updating for other reasons, you can use the special property `copy` to prevent the input element from updating.
If your component needs to update for other reasons, you can use the special `copy` prop to prevent the input element from updating. The `copy` prop is a boolean which prevents an element and children from rerendering.

```jsx live
import {renderer} from "@b9g/crank/dom";
Expand Down
Loading

0 comments on commit 7a9565d

Please sign in to comment.