Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@
"words": [
"afterbegin",
"afterend",
"autofocus",
"beforebegin",
"beforeend",
"beforeunload",
"beresp",
"browsersl",
"browserslist",
"Brrrrr",
"contenteditable",
"dblclick",
"Delisle",
"desugar",
"desugared",
"desugars",
"Fastly",
"figcaption",
"fortawesome",
"hastscript",
"importmap",
"Jetpack",
"jridgewell",
"keymap",
"labelledby",
Expand All @@ -32,6 +38,7 @@
"onbeforeinput",
"openjsf",
"optgroup",
"popovertarget",
"renderable",
"reorder",
"reorderer",
Expand All @@ -43,15 +50,9 @@
"tablist",
"taglib",
"touchstart",
"webp",
"WHATWG",
"beresp",
"TTFB",
"Jetpack",
"popovertarget",
"autofocus",
"browserslist",
"browsersl"
"webp",
"WHATWG"
],
"ignoreRegExpList": [],
"files": ["*", "docs/**/*", "src/**/*"],
Expand Down
32 changes: 31 additions & 1 deletion docs/explanation/controllable-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ Because this is a common pattern, Marko provides a [binding shorthand](../refere
```

> [!NOTE]
> The [binding shorthand](../reference/language.md#shorthand-change-handlers-two-way-binding) acts differently when used with an _identifier_ versus a _member expression_. Above is the identifier behavior; we'll see the member expression behavior next.
> The [binding shorthand](../reference/language.md#shorthand-change-handlers-two-way-binding) acts differently when used with an _identifier_ versus a _member expression_.

### Controllable `<let>`

Expand Down Expand Up @@ -209,6 +209,36 @@ export interface Input {
<output>${count}</output>
```

### Refining Functions

The shorthand may include a [refining function](../reference/language.md#refining-function) that transforms the value before assignment. This is useful when the child component receives a broad type (e.g. native tag attributes may receive `number | string`) but the parent requires something more narrow.

```marko
<let/num=0/>

<input type="range" value:parseFloat:=num>
```

This example desugars to include `parseFloat` in its setter.

```marko
<let/num=0/>

<input type="range" value=num valueChange(v) { num = parseFloat(v)}>
```

This shorthand takes _any_ function, including custom ones.

```marko
<let/text="HELLO">

<input value:uppercase:=text>

static function uppercase(str: string) {
return str.toUpperCase()
}
```

## More Power

The controllable pattern allows the _user_ of a component to decide whether to manage state. Simple cases remain simple, but complex state management is also possible.
Expand Down
22 changes: 22 additions & 0 deletions docs/reference/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,28 @@ For [Property Accessors](https://developer.mozilla.org/en-US/docs/Web/JavaScript
<counter value=input.count valueChange=input.countChange/>
```

#### Refining function

The [change handler shorthand](#shorthand-change-handlers-two-way-binding) may optionally include a function that transforms the value before assignment. The function name appears between colons (`:refiningFunction:=`).

```marko
<input type="number" value:parseFloat:=num>

// desugars to

<input type="number" value=num valueChange(newValue) { num = parseFloat(newValue) }>
```

For [Property Accessors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors), the desugared handler includes a boolean expression.

```marko
<input type="number" value:parseFloat:=input.num>

// desugars to

<input type="number" value=input.num valueChange=(input.num && (newValue) => { input.numChange(parseFloat(newValue)) })>
```

### Shorthand `class` and `id`

[Emmet style](https://docs.emmet.io/abbreviations/syntax/#id-and-class) `class` and `id` attribute shorthands are supported.
Expand Down
42 changes: 25 additions & 17 deletions docs/tutorial/components-and-reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ Of course, right now we aren't keeping track of the value that this input contai

## Syncing State

Now the `<input>` has an initial value, but we still aren't keeping track of it when it changes. One way to do this is by listening for [the `input` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event) with an [event handler](../reference/native-tag.md#event-handlers):
Now the `<input>` has an initial value, but we still aren't keeping track of it when it changes. One way you may think to do this is by listening for [the `input` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event) with an [event handler](../reference/native-tag.md#event-handlers):

```marko
// Warning: There's a better way to do this!
<let/degF=80>

<input type="number" value=degF onInput(e) {
Expand All @@ -38,27 +39,40 @@ Now the `<input>` has an initial value, but we still aren't keeping track of it
<div>It's ${degF}°F</div>
```

Aha! Now we have a [reactive variable](../reference/reactivity.md) that keeps track of our value for degrees (in fahrenheit). Let's convert it to celsius!
This _seems_ to work at first glance, but you'll find out quickly that the value of the input isn't fully synchronized. This is because in HTML, `value=` actually refers to the [_default_ value](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#value) of the input and not its current value. This is why instead, we should leverage the [controllable](../reference/native-tag.md#change-handlers) pattern with `Change` handlers.

> [!NOTE]
> For more control over the `<input>` value, we could have used Marko's [controllable](../reference/native-tag.md#change-handlers) pattern.
```marko
<let/degF=80>

<input type="number" value=degF valueChange(value) { degF = parseFloat(value) }>
<div>It's ${degF}°F</div>
```

Because this is such a common pattern, Marko provides a [shorthand](../reference/language.md#shorthand-change-handlers-two-way-binding) for it!

```marko
<let/degF=80>

<input type="number" value:parseFloat:=degF>
<div>It's ${degF}°F</div>
```

## Adding Computed Values

To do this, we can use a `<const>` tag:
Now we can use [the `<const>` tag](../reference/core-tag.md#const) to convert to celsius!

```marko
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>

<input type="number" value=degF onInput(e) {
degF = +e.target.value;
}>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
```

Since `degC` is a [tag variable](../reference/language.md#tag-variables), its changes also propagate every time `degF` is updated.

## Using Conditionals

Now that we have a reactive variable, let's see what else we can do! Maybe some notes about the temperature, using [conditional tags](../reference/core-tag.md#if--else)?
Expand All @@ -67,9 +81,7 @@ Now that we have a reactive variable, let's see what else we can do! Maybe some
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>

<input type="number" value=degF onInput(e) {
degF = +e.target.value;
}>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
Expand All @@ -93,9 +105,7 @@ Or what about a temperature gauge, with some fancy CSS?
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>

<input type="number" value=degF onInput(e) {
degF = +e.target.value;
}>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
Expand Down Expand Up @@ -136,9 +146,7 @@ Actually, this is getting a little bit too complex to all put in one place. Mayb
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>

<input type="number" value=degF onInput(e) {
degF = +e.target.value;
}>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
Expand Down
Loading