diff --git a/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/answer.gjs b/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/answer.gjs
new file mode 100644
index 000000000..bb89a584f
--- /dev/null
+++ b/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/answer.gjs
@@ -0,0 +1,41 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { on } from '@ember/modifier';
+
+class ControlledInput extends Component {
+ handleInput = (event) => {
+ let input = event.target;
+ let value = input.value;
+
+ this.args.onChange(value);
+ }
+
+
+
+
+}
+
+class State {
+ @tracked value;
+ handleChange = (newValue) => this.value = newValue;
+}
+const createState = () => new State();
+
+
+ {{#let (createState) as |x|}}
+ {{x.value}}
+ {{/let}}
+
+
+
+
diff --git a/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/prompt.gjs b/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/prompt.gjs
new file mode 100644
index 000000000..1b38c7c9a
--- /dev/null
+++ b/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/prompt.gjs
@@ -0,0 +1,36 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { on } from '@ember/modifier';
+
+class ControlledInput extends Component {
+
+
+
+}
+
+// Some state is defined, and then created in the template
+class State {
+ @tracked value;
+ handleChange = (newValue) => this.value = newValue;
+}
+const createState = () => new State();
+
+
+ {{#let (createState) as |x|}}
+ {{x.value}}
+ {{/let}}
+
+
+
+
diff --git a/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/prose.md b/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/prose.md
new file mode 100644
index 000000000..4b90f2366
--- /dev/null
+++ b/apps/tutorial/public/docs/8-form-data-controlled/1-text-input/prose.md
@@ -0,0 +1,32 @@
+An _uncontrolled_ input is what we've used so far, where the inputs themselves manage their own internal state. We do not control that state, and so they are _uncontrolled_.
+
+A _controlled_ input is controlled by the invoking component.
+
+There are two parts to controlling a component: managing the value, and responding to an event on the input (which usually sets that managed value).
+
+
+Managing the value is the same as we've seen before with setting an input's initial value:
+```hbs
+
+```
+
+The second part, responding to an event on the input, usually unwraps the event and passes the current value to the calling component:
+```gjs
+class Demo extends Component {
+ handleInput = (event) => {
+ this.args.onChange(event.target.value);
+ }
+
+
+
+
+}
+```
+
+Because, with a text input, we expect a string to be passed to an `onChange` handler, we have nothing more to do.
+
+
+ Change the input within the ControlledInput
component
+ to be controlled.
+
{{x.value}}+ {{/let}} + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/2-checkbox/prompt.gjs b/apps/tutorial/public/docs/8-form-data-controlled/2-checkbox/prompt.gjs new file mode 100644 index 000000000..743862101 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/2-checkbox/prompt.gjs @@ -0,0 +1,36 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; + +class ControlledInput extends Component { + + + +} + +// Below is only setup for the tutorial chapter +// and not exactly relevent to the topic +class State { + @tracked value; + handleChange = (newValue) => this.value = newValue; +} +const createState = () => new State(); + + + {{#let (createState) as |x|}} +
{{x.value}}+ {{/let}} + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/2-checkbox/prose.md b/apps/tutorial/public/docs/8-form-data-controlled/2-checkbox/prose.md new file mode 100644 index 000000000..dff6f0432 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/2-checkbox/prose.md @@ -0,0 +1,41 @@ +Just like the controlled input, the _controlled checkbox_ is roughly the same approach, but with a different property and event binding combination. + +Instead of setting value, we'll set `checked`. +```hbs + +``` + +And for the event binding, we'll use the `change` event. +```gjs +class Demo extends Component { + handleChange = (event) => { + let value = Boolean(event.target.checked); + this.args.onChange(value); + } + + + + +} +``` + +Checkboxes have a default `value` of `on` (as a string), which may not be a desired value to pass to your `onChange` handler. In this example, we can convert the `checked` property to a boolean. + +
+ Change the checkbox within the ControlledInput
component
+ to be controlled.
+
{{x.value}}+ {{/let}} + + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/3-radio/prompt.gjs b/apps/tutorial/public/docs/8-form-data-controlled/3-radio/prompt.gjs new file mode 100644 index 000000000..7cfdad46d --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/3-radio/prompt.gjs @@ -0,0 +1,54 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; + +class ControlledInput extends Component { + + + +} + +// Below is only setup for the tutorial chapter +// and not exactly relevent to the topic +class State { + @tracked value; + handleChange = (newValue) => this.value = newValue; +} +const createState = () => new State(); + + + {{#let (createState) as |x|}} +
{{x.value}}+ {{/let}} + + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/3-radio/prose.md b/apps/tutorial/public/docs/8-form-data-controlled/3-radio/prose.md new file mode 100644 index 000000000..3fe215064 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/3-radio/prose.md @@ -0,0 +1,49 @@ +Just like the controlled input, the _controlled radio button_ is roughly the same approach (conceptually), but we because we now have a list of options where only one can be active at a time, the way in which we set the "selected option" as well as how we handle the events will be very different. + +Instead of setting value on a single input, we'll set `checked` to be the result of a function call on _each option_: +```gjs +class Demo extends Component { + isChecked = (value) => this.args.value === value + + + + + +} +``` + +And for the event binding, we'll use the `change` event on each of the radio inputs as well. +```gjs +class Demo extends Component { + handleChange = (event) => { + let radio = event.target; + + this.args.onChange(radio.value); + } + + + + + +} +``` + +
+ Change the radio buttons within the ControlledInput
component
+ to be controlled.
+
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/4-select/prompt.gjs b/apps/tutorial/public/docs/8-form-data-controlled/4-select/prompt.gjs new file mode 100644 index 000000000..b85bbd7fe --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/4-select/prompt.gjs @@ -0,0 +1,41 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; + +class ControlledInput extends Component { + + + +} + +// Below is only setup for the tutorial chapter +// and not exactly relevent to the topic +class State { + @tracked value; + handleChange = (newValue) => this.value = newValue; +} +const createState = () => new State(); + + + {{#let (createState) as |x|}} +
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/4-select/prose.md b/apps/tutorial/public/docs/8-form-data-controlled/4-select/prose.md new file mode 100644 index 000000000..770398009 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/4-select/prose.md @@ -0,0 +1,40 @@ +Just like the controlled input, the _controlled select_ is roughly the same approach (conceptually), but we because we now have a list of options where only one can be active at a time, the way in which we set the "selected option" as well as how we handle the events will be very different. + +Instead of setting value on a single input, we'll set `selected` to be the result of a function call on _each option_: +```gjs +class Demo extends Component { + isSelected = (value) => this.args.value === value + + + + +} +``` + +And for the event binding, we'll use the `change` event on the single select element. +```gjs +class Demo extends Component { + handleChange = (event) => { + let select = event.target; + + this.args.onChange(select.value); + } + + + + +} +``` + +
+ Change the select within the ControlledInput
component
+ to be controlled.
+
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/5-select-multiple/prompt.gjs b/apps/tutorial/public/docs/8-form-data-controlled/5-select-multiple/prompt.gjs new file mode 100644 index 000000000..eb2e4023f --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/5-select-multiple/prompt.gjs @@ -0,0 +1,42 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; + +class ControlledInput extends Component { + + + +} + +// Below is only setup for the tutorial chapter +// and not exactly relevent to the topic +class State { + @tracked value; + handleChange = (newValue) => this.value = newValue; +} +const createState = () => new State(); + + + + {{#let (createState) as |x|}} +
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/5-select-multiple/prose.md b/apps/tutorial/public/docs/8-form-data-controlled/5-select-multiple/prose.md new file mode 100644 index 000000000..e5d73051a --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/5-select-multiple/prose.md @@ -0,0 +1,44 @@ +Making a select multiple field _controlled_ is nearly the same as making a non-multiple select field controlled -- the main difference is that we now need to deal with array data (manually). + +Our value now represents an array of known options, rather than a single value, so our `isSelected` function must be updated: +```gjs +class Demo extends Component { + // Note the `?.` because the initial value isn't set. + isSelected = (value) => this.args.value?.includes(value); + + + + +} +``` + +And for the event binding, we'll use the `change` event on the single select element. However, this time we need to do some processing to figure out what the selected array is. +```gjs +class Demo extends Component { + handleChange = (event) => { + let select = event.target; + let selected = [...select.options] + .filter(option => option.selected) + .map(option => option.value); + + this.args.onChange(selected); + } + + + + +} +``` + +
+ Change the select within the ControlledInput
component
+ to be controlled.
+
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/6-textarea/prompt.gjs b/apps/tutorial/public/docs/8-form-data-controlled/6-textarea/prompt.gjs new file mode 100644 index 000000000..d92a41dc7 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/6-textarea/prompt.gjs @@ -0,0 +1,45 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; + +class ControlledInput extends Component { + handleChange = (event) => { + let textarea = event.target; + + this.args.onChange(textarea.value); + } + + + + +} + +// Below is only setup for the tutorial chapter +// and not exactly relevent to the topic +class State { + @tracked value = "initial text"; + handleChange = (newValue) => this.value = newValue; +} +const createState = () => new State(); + + + + {{#let (createState) as |x|}} +
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/6-textarea/prose.md b/apps/tutorial/public/docs/8-form-data-controlled/6-textarea/prose.md new file mode 100644 index 000000000..5999977b4 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/6-textarea/prose.md @@ -0,0 +1,32 @@ +Making the textarea element _controlled_ is similar to a text input. The only difference is where the value is set. + +We can set the value by placing it in the content of the textarea element. +```gjs +class Demo extends Component { + + + +} +``` + +And for the event binding, we can use the `input` event to have live updates as we type: +```gjs +class Demo extends Component { + handleChange = (event) => { + let textarea = event.target; + + this.args.onChange(textarea.value); + } + + + + +} +``` + +
+ Change the textarea within the ControlledInput
component
+ to be controlled.
+
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/7-contenteditable/prompt.gjs b/apps/tutorial/public/docs/8-form-data-controlled/7-contenteditable/prompt.gjs new file mode 100644 index 000000000..474ae7810 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/7-contenteditable/prompt.gjs @@ -0,0 +1,52 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { on } from '@ember/modifier'; +import { fn } from '@ember/helper'; +import { modifier } from 'ember-modifier'; + +let { document } = globalThis; +const bold = () => document.execCommand("bold", false, null); +const italic = () => document.execCommand("italic", false, null); +const underline = () => document.execCommand("underline", false, null); +const list = () => document.execCommand("insertUnorderedList", false, null); + +class ControlledInput extends Component { + + + + + + + + +} + +// Below is only setup for the tutorial chapter +// and not exactly relevent to the topic +class State { + @tracked value = "initial text"; + handleChange = (newValue) => this.value = newValue; +} +const createState = () => new State(); + + + + {{#let (createState) as |x|}} +
{{x.value}}+ {{/let}} + + + + + diff --git a/apps/tutorial/public/docs/8-form-data-controlled/7-contenteditable/prose.md b/apps/tutorial/public/docs/8-form-data-controlled/7-contenteditable/prose.md new file mode 100644 index 000000000..ddbec06b8 --- /dev/null +++ b/apps/tutorial/public/docs/8-form-data-controlled/7-contenteditable/prose.md @@ -0,0 +1,67 @@ +Because contenteditable has browser-implemented internal state, it's not *exactly* able to be controlled. But we can set the initial value, and listen to updates as they happen. + + +For setting the initial value we need to _disconnect_ from auto-tracking. +This can be done, inline, with an async immediately invoked function execution (IIFE), +```js +(async () => { + await Promise.resolve(); + // code that access tracked state here + // ... +})() +``` +This defers execution of "the real code" to the "next" [microtask](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth). +We also need to get a reference to the element, so we'll need to create a modifier for where our async IIFE can live: +```js +import { modifier } from 'ember-modifier'; + +const setContent = modifier((element, [initialize]) => { + (async () => { + // Disconnect from auto-tracking + // so we can only set the inner HTML once + await Promise.resolve(); + initialize(element); + })(); +}); +``` +But then we need to use the modifier in our contenteditable: +```gjs +class Demo extends Component { + setValue = (element) => element.innerHTML = this.args.value; + + + + +} +``` + +And for the event binding, we can use the `input` event to have live updates as we type: +```gjs +class Demo extends Component { + handleChange = (event) => { + let textarea = event.target; + + this.args.onChange(textarea.innerHTML); + } + + + + +} +``` + +Unlike the other controlled inputs we've covered so far, we dan't expect to do anything with the updated `@value` once it's passed back in to our `contenteditable` component. `contenteditable` has its own state, and if we were to retain an fully _controlled_ `@value`, we would then need to manage the cursor position within the contenteditable element, and that is a lot of code. + +
+ Change the contenteditable within the ControlledInput
component
+ to have an initial value set and an input event listener.
+