# Controllable components <style> .reveal { --r-main-color: #ddd; } .reveal em, .reveal strong { color:#fff; } .reveal code:not(pre code) { color: #99a9ff; } .reveal table { margin-top:1em; } iframe { margin-top:0.5em; width:100%; border:0; border-radius: 4px; } .reveal .slides section[data-markdown] { top: 0 !important; height: 100%; } .reveal .slides section[data-markdown] > div { display: flex; flex-direction: column; justify-content: center; align-items: center; height:100%; box-sizing:border-box; padding:1em 0; } .reveal iframe { flex: 1; } </style> --- ## Uncontrolled components Mostly work like native stateful HTML elements: - Own and manage their state (“self-controlled”) - May accept an initial state - May provide an imperative API to view/update their state - May emit events when their state changes <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAedgFdpBAARoUtFFAA2AYRLLaAXgCMzAHxgAvuXDR4TJSryFSFanUbI2HLjz5BZ1o4AldxDykQvnk6WgJBEHUITW19Qz5VY1UABlVrSwAdCDkdOFpfI2MNLV0DIyKSgCNFWlpuVW49HRRWIgAKAEpVJOCAajH8i2LVVQASEGDrYrlmFrbuK2sAXSA"></iframe> --- ## Controlled components - Receive their current state from parent - Cannot directly change their own state - Request state changes through events/callbacks, which are handled by their parent (the parent updates state in response to the events) <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxabEgFc+AAgC8mgIzMAfAB0Ic9htpwCmi3213am7gEkIrAnHh8AFAEpNEFt1PgBqUM0AXyMwSPJwaHgmR2s8QlIKajpGZFVObhlBFII08UypKz55OloCQSDHcmcINw8vGSijUzkAIzVaWm5mgGEFFFYibVd3T29aEwhNTQASEEdIpaXu5j6B7kNYgF0gA"></iframe> --- ## When to use controlled vs uncontrolled components Using an uncontrolled component is much simpler than using a controlled component, as can be seen from the above `<counter>` example. However, controlled components give more _control_ (go figure) over the state. If you don't need that control, uncontrolled components are more desirable. --- But, sometimes you do need that control: > "Sometimes, you want the state of two components to always change together. To do it, remove state from both of them, move it to their closest common parent, and then pass it down to them via props. This is known as lifting state up, and it’s one of the most common things you will do writing React code." --- [React Beta Docs](https://beta.reactjs.org/learn/sharing-state-between-components) --- You might want to sync the state somewhere else, like a heading: <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxabEgFc+AAgC8mgAzMAfAB0Ic+gEZDAFXpxNrNQQIyH6rSn6aAJCHYbaAF85ZksTM39pAk1TTTi3AO1I2hiIeM1uAEkIVhd4PgAKAEpNEAS+AGoKzUCjMEDycGh4JmS4AjxCUgpqOkZkVU5uGUE2jrFuyW5aGXk6WgJBMuTyDIhs3Lh8lNrwuQAjNVpabjWAYQUUViJtLJy8mXC432TA9NMQw+PuQ3qAXSAA"></iframe> --- Or maybe you'd like to restrict or augment state updates, like capping the counter to 9: <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxabEgFc+AAgC8mgAzMAfAB0Ic9htpwCm05vuaLfbU9p2H3AJIRWBOPD4ACgBKTRBHdS1dAFk6ejw0QIBOcgjLTQBqTQBGUIBfI1MwPPJwaHgmV2s8QlIKajimVU5uGUEqghrxeqkrPnk6WgJBcNdUrx8-ANpNApMzACM1WlpuTW4AYQUUViJtCd9-GXn7ABIQVzyHWzNmJZXuQ2KAXSA"></iframe> --- Or update state from outside the counter, like a button that resets the count to 0: <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxabEgFc+AAgC8mgAzMAfAB0Ic9htpwCm05vuaLfbU9p2H3AJIRWBOPD4ACgBKTRBHdT4AaijNAF8jU1M5ACM1WlpuTW4AYQUUViIQsIjLHX14kwgHTQAlOH5lZOY0jO4qsDjycGh4JldrPEJSCmo6RmRVTm4ZQQGCIfFRqSs+eTpaAkFw13JsiG9ffxl4xLNWzOrc-MLtLx8-ANoq+wASEFc4muaL9s6AXSAA"></iframe> --- ## Controllable components You may have noticed --- whether we want to control a component or not can depend on its usage. The component itself may not be in the best position to make that decision. The safe bet would be to go with controlled since it’s the most flexible, but that makes basic use-cases unnecessarily verbose. <!-- **TODO:** can we get an example of this anti-pattern from a React component lib or somewhere? [React-Bootstrap / Accordion § Custom Toggle with Expansion Awareness](https://react-bootstrap.github.io/components/accordion/#custom-toggle-with-expansion-awareness) ```jsx= function ContextAwareToggle({ children, eventKey, callback }) { const { activeEventKey } = useContext(AccordionContext); const decoratedOnClick = useAccordionButton( eventKey, () => callback && callback(eventKey), ); const isCurrentEventKey = activeEventKey === eventKey; return ( <button type="button" style={{ backgroundColor: isCurrentEventKey ? 'pink' : 'lavender' }} onClick={decoratedOnClick} > {children} </button> ); } ``` --> Can we make components that can operate in both uncontrolled and controlled modes? Yes we can! --- ### Controllable `<let>` It turns out, the `<let>` tag itself is controllable! The "normal" usage is uncontrolled --- it maintains its own state and lets you imperatively update it from events and effects. ```jsx= <let/count = 0/> ``` --- However, if you pass a `valueChange` handler… instead of updating its own state when you assign to its variable, it will call `valueChange` with the assigned value: <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxabEgFc+AAgC8mgAyaAblAVq4AYXpQIAczgAKQwEpNIADoRNmk3AK17AAYAmure-ERw6JoAZiQEmgAkIIYAvgFOANweKcwAfB5yAEZqtLTcmtzmCiisRPYuIJrsGrQA1K2aKfmeiSDNfCkFzMWl3LlgKQC6QA"></iframe> --- #### Delegating control Locally controlling `<let>` from inside the component isn't _that_ interesting, but we can also _delegate_ control: ```htmlembedded= <const/{ count, countChange } = input/> <let/internalCount=count valueChange=countChange/> ``` And that can be made more succinct with the [binding syntax (`:=`)](#Binding-Syntax-): ```htmlembedded= <const/{ count } = input/> <let/internalCount:=count/> ``` --- Now, if the parent passes `countChange`, our `<counter>` operates as a _controlled_ component. Otherwise it's _uncontrolled_. In other words: **it is controllable**! <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeegEYAfABV6KfgAJ2AV2kEtmrfqm0CJADaW46OcyXKAOhDl6DOkvtoBeRc2cIFzkbWjYvPi0fLQAGAODHdTh+OE9vOAJtQlT+AE8IVlstOi0AEhB3WgBfe0dgyoy0vkQfSvjXBsNKlrblMCrycGh4Jk68QlIKajpGZHDObhlBMbFJyW5aGXk6c0EQJtoo2K0q9pC4MLRNgmhLAGEI2h7Hs4AjXVpabi1uO8sUVhEAAUAEotPsrhlbg9vABqWEnQJaMogSE3KD3R5VYLMd6fbh9KoAXSAA"></iframe> --- ### Controllable HTML elements The native HTML elements unfortunately don't fit nicely into this paradigm. They're _mostly_ uncontrolled. For example, an `<input>`'s `value` attribute reflects its `defaultValue` property, _not_ its current value. <iframe loading="lazy" src="https://markojs.com/playground/v6/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxabEgFc+AXgAMzAHwAdCHIBGa2rW4ACbqwUpWRABQBKKyCvsNtANQ+rAL6GEFZWACQgXnwBRnLMZhbcwXJoVOZWAG5QCmpwmlG0emABALpAA"></iframe> --- Even elements with 1:1 mapping of attributes↔state, like `<details>`, can get out of sync with the state meant to control them. <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxazElV4ACALwaAZlAX84zAHwAdCHMy0oKQxrW8tjiOYgaNAFXop+G3xoMcBpStDIWcszWtoZucii6zuquFh5yAEYArrS03A4QAMIKKKxEABQAlBogDsnaegZGGgC+bh4eABIomKkakVk53HFCunFwhnBtfQO57txFJeVVNS71tASZwa29HgDK9CQA7r392bPD40YmYM0AukA"></iframe> --- React takes a unique, heavy-handed approach: force the controlled paradigm on native elements (or at least form inputs). <iframe loading="lazy" src="https://codesandbox.io/embed/react-controlled-input-forked-uleknq?fontsize=14&hidenavigation=1&theme=dark&view=editor"></iframe> --- We want the ability to use native elements in a controlled manner, but Marko doesn't change the behavior of HTML by default. The answer? _Directives_. --- #### Directives - look like attributes prefixed with `#` - not real attributes - run at compile-time - can transform into almost _anything_ - encapsulate logic and apply it to existing tags <table><thead><tr><th>For example, this…</th><th>could transform into…</th></thead> <tr><td valign=top> ```htmlembedded= <Photo #if=show /> ``` </td> <td> ```jsx= <if=show> <Photo/> </if> ``` </td> </table> --- In Marko 4 & 5, we had special attributes that _could have_ been directives, such as: ```htmlembedded= <input #no-update/> ``` ```htmlembedded= <for|item| of=array> <component data=item #key=item.id/> </for> ``` --- Marko provides some directives that let you control native elements, while making it clear this is not standard DOM behavior. For each of these control directives, `#___`, there is a corresponding `#___Change` directive so it can be _controllable_ and work with the bind syntax. --- Many of these directives are named after corresponding DOM properties. For example, `#value` and `#valueChange`: ```jsx= <let/name = "Michael"/> <input #value:=name/> ``` Or, `#open` and `#openChange`: ```jsx= <let/show = true/> <details #open:=show>Hello</details> ``` --- ### Controlling a group Some properties, like `checked` on `<input type=radio>`, have state implicitly shared across multiple tags. We want to control `checked`, but we want to control it based on the _value_ of the currently-selected radio. --- In this case, we can use `#checkedValue` and `#checkedValueChange`: ```htmlmixed= <let/num = 1/> <input type="radio" value=1 #checkedValue:=num> 1 <input type="radio" value=2 #checkedValue:=num> 2 <input type="radio" value=3 #checkedValue:=num> 3 <div>Selected: ${num}</div> ``` <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxazAG5QFAVzgBeADpgAjAeYA+PRAuKoAIzgLzEAARO5AJSjoUJJ63pxWIjh0ADUNbUQddS04J2jtfSMTUydjCDlmBVt7R2s7BwsXd09vX39A4LCYyPjY2sSAJmSnJvTM7IL0rPzHIo8vHz8AoNDwuBqxuLHEgGZmubbunIsrL1VTAGV7ANpgxCcAEhBagF8MtccrfloATyVelyclhScQJy9+KiybxBsFEkCThOVmY1zucFMYBO5HA0HgTH63jwhFIFGodEYyDYHC4PD4gkRJGR4jRUl2fHkdFoBEEbzhcHIZWGlTGjNqQLMViUKjQuwI0AUAGFyiMqhEdEMKqMYpz0mgqJpaIVHrcqLoDAQSiQDMqXPSdPTdVMYlExkbJSNTTEnDpbU5eXB+RphczpdpzSLgsKoBAAOZwAAUqgAlK97dInULPW7Yjo4k4APzG7ROfaaDBwABmaGCQMhJwAukA"></iframe> --- If we use checkboxes and there's multiple items in the group, we can use `#checkedValues` and `#checkedValuesChange`: ```htmlmixed= <let/nums = []/> <input type="checkbox" value=1 #checkedValues:=nums> 1 <input type="checkbox" value=2 #checkedValues:=nums> 2 <input type="checkbox" value=3 #checkedValues:=nums> 3 <div>Selected: ${nums}</div> ``` <iframe loading="lazy" src="https://markojs.com/playground/#NobwRAdghgtgpmAXGAlhAJnAHgOhlAJwGsB7MAGjAAcoAXACyTAHoBjEmKkiOCWgZ2ZpMufMTKV2fXrSYAeADZxazAG5QFAVzj8AvMAC6zAHwAdCIqgAjOArMQABA7kBhenFZErJLA9bvPOHQANQ1tfkRddS0dB2jtXVMwAEYkkwdUi2YFa1t7Sxs7cydXAK8fPzKg0JiIqLDY+LhEsAAmNOMHdqycwvzevOLnNw9y339R6oa6pv44hpaAZg6HZZ7coohzOXQUVWMAZVsPWiDEBwASEFmAXzlmXf3zbf5aAE8leycnAYUHEAcu34VByb0QVgUJE8Dhu22Yrw+cGMYBu5HA0HgTBGnm8okIpAo1DojGQbA4XB4fEE2LGeHxEjAUlOfHkdFoBEEAIxcHIlUmIWmvKaMJM2yUKjQpwI0AUNKmtUiE0CAtqoosaComloQyc7yozSSSrGSR1Dm5um5pqa9RipqNQV0krg0o0cpV4RwaFYWkw-AAFE0AJR2qroNxQCAAczgAcD-wcpu+DidLtloZq4QcujiCccSfzTgA-A5gDgyymZW6MzohQ0DLmCwXzhXXenpjgAGYoBRSv0AfSznQHAEJdNmgw3vjdkTcDEA"></iframe> --- ### Wrapping native elements Custom tags can provide the same API, but as regular attributes, by dropping the `#`. For example: ```htmlembedded= <input #value:=foo/> ``` becomes ```htmlembedded= <fancy-input value:=foo/> ``` --- ### Control directives list <small> | Directive | Property | Applicable Tags | | ----------------- | --------------- | ----------------------------------------------------------------- | | `#value` | `value` | `input` (not radio/checkbox), `textarea` | | `#valueAsNumber` | `valueAsNumber` | `input[type=number]`, `input[type=date]`, `input[type=range]`, etc. | | `#valueAsDate` | `valueAsDate` | `input[type=date]`, etc. | | `#checkedValue` | `checked` | `input[type=radio]` | | `#checkedValues` | `checked` | `input[type=checkbox]` | | `#selectedValue` | `selected` | `select option` | | `#selectedValues` | `selected` | `select[multiple] option` | | `#open` | `open` | `details`, `dialog` | </small> --- ## Binding Syntax (`:=`) A change handler may be added to _any_ attribute in Marko by replacing its `=` with a `:=`. This syntax introduces no new behavior, but instead is a shorthand that automatically adds a `Change` attribute. ### Object ```marko <let/count:=input.count/> // desugars to <let/count=input.count countChange=input.countChange/> ``` ### Tag variable Since tag variables are known at compile time, Marko has the freedom to introduce unique behavior for the change handler. ```marko <let/x=0/> <counter count:=x/> // desugars to <counter count=x countChange(newCount) { x = newCount } ``` ### Tag variable with member expression If a bound tag variable has an attached member expression, cloning is handled automatically. ```marko <let/state={ user: { name: "Michael", id: 1234, } }/> <editable name:=state.user.name /> // desugars to <editable name=state.user.name nameChange(newName) { state = { ...state, { user: { ...state.user, name: newName } } } } /> ``` This may also be used to edit large objects with more granularity, which is especially useful deeper in the tree or inside loops. ```marko <let/todo={ items: [ { name: "one" }, { name: "two" }, { name: "three" }, ] }/> <for|i| in=todo.items> <let/item:=todo.items[i] /> <input value:=item.name /> </for> // desugars to <for|i| in=todo.items> <let/item=todo.items[i] valueChange(newItem) { todo = { ...todo, items: todo.items.toSpliced(i, 1, newItem) } } /> <input value=item.name onChange({value}) { item = { ...item, name: value } } /> </for> ``` The `for` tag also handles `ofChange`, so the above can be simplified even further. ```marko <for|item| of:=todo.items> <input value:=item.name/> </for> // desugars to <for|item| of=todo.items ofChange(newItems) { todo = { ...items: newItems } }> <input value=item.name valueChange({value}) { item = { ...item, name: value } } /> </for> ```