Skip to content

Commit

Permalink
Controlled Inputs (#1758)
Browse files Browse the repository at this point in the history
* Start working on a section for controlled inputs

* text input done

* Add checkbox

* Radio

* Add select

* Add select multiple

* Textarea

* Contenteditable
  • Loading branch information
NullVoxPopuli authored May 28, 2024
1 parent e236151 commit 2f9d2c9
Show file tree
Hide file tree
Showing 21 changed files with 983 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}

<template>
<label>
Example input
<input type="text" value={{this.value}} {{on 'input' this.handleInput}} />
</label>
</template>
}

class State {
@tracked value;
handleChange = (newValue) => this.value = newValue;
}
const createState = () => new State();

<template>
{{#let (createState) as |x|}}
<ControlledInput @value={{x.value}} @onChange={{x.handleChange}} />

<pre>{{x.value}}</pre>
{{/let}}

<style>
input {
border: 1px solid;
padding: 0.25rem 0.5rem;
}
</style>
</template>

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

class ControlledInput extends Component {
<template>
<label>
Example input
{{! Make this input a controlled input }}
<input type="text" />
</label>
</template>
}

// Some state is defined, and then created in the template
class State {
@tracked value;
handleChange = (newValue) => this.value = newValue;
}
const createState = () => new State();

<template>
{{#let (createState) as |x|}}
<ControlledInput @value={{x.value}} @onChange={{x.handleChange}} />

<pre>{{x.value}}</pre>
{{/let}}

<style>
input {
border: 1px solid;
padding: 0.25rem 0.5rem;
}
</style>
</template>

Original file line number Diff line number Diff line change
@@ -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
<input value={{@value}} />
```

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);
}
<template>
<input value={{@value}} {{on 'input' this.handleInput}} />
</template>
}
```

Because, with a text input, we expect a string to be passed to an `onChange` handler, we have nothing more to do.

<p class="call-to-play">
Change the input within the <code>ControlledInput</code> component
to <strong>be controlled</strong>.
</p>

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

class ControlledInput extends Component {
handleChange = (event) => {
let input = event.target;
let value = Boolean(input.checked);

this.args.onChange(value);
}

<template>
<label>
Example input
<input
type="checkbox"
checked={{this.value}}
{{on 'change' this.handleChange}} />
</label>
</template>
}

// 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();

<template>
{{#let (createState) as |x|}}
<ControlledInput @value={{x.value}} @onChange={{x.handleChange}} />

<pre>{{x.value}}</pre>
{{/let}}

<style>
input {
border: 1px solid;
padding: 0.25rem 0.5rem;
}
</style>
</template>

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

class ControlledInput extends Component {
<template>
<label>
Example input
<input type="checkbox" />
</label>
</template>
}

// 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();

<template>
{{#let (createState) as |x|}}
<ControlledInput @value={{x.value}} @onChange={{x.handleChange}} />

<pre>{{x.value}}</pre>
{{/let}}

<style>
input {
border: 1px solid;
padding: 0.25rem 0.5rem;
}
</style>
</template>

Original file line number Diff line number Diff line change
@@ -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
<input type="checkbox" checked={{@value}} />
```

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);
}
<template>
<input
type="checkbox"
value={{@value}}
{{on 'change' this.handleChange}} />
</template>
}
```

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.

<p class="call-to-play">
Change the checkbox within the <code>ControlledInput</code> component
to <strong>be controlled</strong>.
</p>


### References

- [Checkbox Inputs at MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox)

API Docs:
- [let](https://api.emberjs.com/ember/5.8/classes/Ember.Templates.helpers/methods/let?anchor=let)
- [on](https://api.emberjs.com/ember/5.8/classes/Ember.Templates.helpers/methods/on?anchor=on)
- [Component](https://api.emberjs.com/ember/5.8/modules/@glimmer%2Fcomponent)
- [tracked](https://api.emberjs.com/ember/5.8/functions/@glimmer%2Ftracking/tracked)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

class ControlledInput extends Component {
handleChange = (event) => {
let radio = event.target;

this.args.onChange(radio.value);
}

isChecked = (value) => this.args.value === value

<template>
<fieldset>
<legend>Favorite Race</legend>
<label>
Zerg
<input
type="radio" name="bestRace" value="zerg"
checked={{this.isChecked "zerg"}}
{{on 'change' this.handleChange}}
/>
</label>
<label>
Protoss
<input
type="radio" name="bestRace" value="protoss"
checked={{this.isChecked "protoss"}}
{{on 'change' this.handleChange}}
/>
</label>
<label>
Terran
<input
type="radio" name="bestRace" value="terran"
checked={{this.isChecked "terran"}}
{{on 'change' this.handleChange}}
/>
</label>
</fieldset>
</template>
}

// 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();

<template>
{{#let (createState) as |x|}}
<ControlledInput @value={{x.value}} @onChange={{x.handleChange}} />

<pre>{{x.value}}</pre>
{{/let}}


<style>
fieldset > label {
display: block;
}
</style>

</template>

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

class ControlledInput extends Component {
<template>
<fieldset>
<legend>Favorite Race</legend>
<label>
Zerg
<input
type="radio" name="bestRace" value="zerg"
/>
</label>
<label>
Protoss
<input
type="radio" name="bestRace" value="protoss"
/>
</label>
<label>
Terran
<input
type="radio" name="bestRace" value="terran"
/>
</label>
</fieldset>
</template>
}

// 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();

<template>
{{#let (createState) as |x|}}
<ControlledInput @value={{x.value}} @onChange={{x.handleChange}} />

<pre>{{x.value}}</pre>
{{/let}}


<style>
fieldset > label {
display: block;
}
</style>

</template>

Loading

0 comments on commit 2f9d2c9

Please sign in to comment.