diff --git a/website/documents/guides/01-getting-started.md b/website/documents/guides/01-getting-started.md index 7204e471..4482a632 100644 --- a/website/documents/guides/01-getting-started.md +++ b/website/documents/guides/01-getting-started.md @@ -214,6 +214,4 @@ export default defineConfig({ }); ``` -## Shovel - -A full-stack framework is in the works for Crank. Stay tuned. +## Key Examples diff --git a/website/documents/guides/02-elements.md b/website/documents/guides/02-elements.md index a6007bfd..19e21893 100644 --- a/website/documents/guides/02-elements.md +++ b/website/documents/guides/02-elements.md @@ -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. @@ -83,7 +83,6 @@ console.log(html); //
Hello world
## The Parts of an 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 (`
` not `
`). 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. @@ -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. diff --git a/website/documents/guides/03-components.md b/website/documents/guides/03-components.md index 58c388d9..19eb2db1 100644 --- a/website/documents/guides/03-components.md +++ b/website/documents/guides/03-components.md @@ -2,9 +2,12 @@ title: Components --- -So far, we’ve only seen and used *host elements*, lower-case elements like `` or `
`, 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 `` or `
`, 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 @@ -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++; @@ -84,26 +87,50 @@ function *Timer() { try { while (true) { yield ( -
Seconds elapsed: {seconds}
+
{message} {seconds}
); } } finally { clearInterval(interval); } } + +renderer.render(, document.body); ``` -This `` component is similar to the `` 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 `` component is similar to the `` 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 `` 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 ( +
{message} {seconds}
+ ); + } + } 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 `` 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++; @@ -120,27 +147,25 @@ function *Timer({message}) { } renderer.render( - , + , document.body, ); setTimeout(() => { renderer.render( - , + , 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++; @@ -160,17 +185,18 @@ renderer.render(, 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
Hello, {name}
; } -renderer.render(, document.body); // "
Hello World
" +renderer.render(, 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"; diff --git a/website/documents/guides/04-handling-events.md b/website/documents/guides/04-handling-events.md index 4dbf3c4c..b6f6195f 100644 --- a/website/documents/guides/04-handling-events.md +++ b/website/documents/guides/04-handling-events.md @@ -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"; @@ -83,13 +83,6 @@ function *Counter() { renderer.render(, 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: @@ -110,7 +103,7 @@ function MyButton(props) { function MyButtons() { return [1, 2, 3, 4, 5].map((i) => ( -

+

Button {i}

)); @@ -179,7 +172,14 @@ function *CustomCounter() { renderer.render(, 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 @@ -213,7 +213,7 @@ function *Form() { renderer.render(
, 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"; @@ -237,7 +237,7 @@ function *Form() { reset = false; yield ( - +

diff --git a/website/documents/guides/05-async-components.md b/website/documents/guides/05-async-components.md index 80f092ae..66785981 100644 --- a/website/documents/guides/05-async-components.md +++ b/website/documents/guides/05-async-components.md @@ -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"; @@ -25,7 +25,7 @@ async function Definition({word}) { await renderer.render(, 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: @@ -83,7 +83,7 @@ renderer.render(, 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 await…of` 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. @@ -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 ; yield ; } } -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 (
@@ -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() {
); @@ -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 ( +
+ {" "} + + +
+ ); + } +} + +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 ; + // 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 ( +
+
+ {initial || } +
+
+ +
+
+ ); + + 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 ( +
+ + +
+ ); + } catch (err) { + yield ( +
+
Error: {err.message}
+ +
+ ); + } } } -``` -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() {
{/* - + */}
); @@ -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 (
- +
) @@ -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 (
- {" "}