Two-Way Binding
Big chunks of code in a progressive web application is dedicated to reacting to user input.
The process of controlling user input can be broken into 3 tedious steps:
- Declaring a variable with the initial value;
- Passing the initial value to the input;
- Observing changes in the input and assigning the new value to the variable.
The last step might include typecasting and other value treatments.
This process in which you manually do all these steps is called one-way binding, it is the default in many frameworks, and is possible in Nullstack.
import Nullstack from 'nullstack';
class Form extends Nullstack {
number = 1;
string = '';
updateString({event}) {
this.string = event.target.value;
}
updateNumber({event}) {
this.number = parseInt(event.target.value);
}
render() {
return (
<form>
<input
type="text"
name="string"
value={this.string}
oninput={this.updateString}
/>
<input
type="number"
name="number"
value={this.number}
oninput={this.updateNumber}
/>
</form>
)
}
}
export default Form;
The bind attribute
Bind reduces drastically the amount of glue code you have to type in your application.
You can shortcut setting a value
, name
, and event with the bind
attribute.
💡 Nullstack will simply replace
bind
with thevalue
,name
, and respective event under the hood at transpile time.
Bind will generate an event that automatically typecasts to the previous primitive type the value was.
You can pass any variable to the bind
as long as its parent object is mentioned.
import Nullstack from 'nullstack';
class Form extends Nullstack {
number = 1;
string = '';
render() {
return (
<form>
<input type="text" bind={this.string} />
<input type="number" bind={this.number} />
</form>
)
}
}
export default Form;
Bound Events
The following events are set for each type of input:
onclick
for inputs with the checkbox typeoninput
for other inputs and textareasonchange
for anything else
You can still declare an attribute with the same bound event.
Events will not override the bound event, instead, it will be executed after bind mutates the variable.
The new value will be merged into the function context.
import Nullstack from 'nullstack';
class Form extends Nullstack {
name = '';
compare({value}) {
this.name === value; // true
}
render() {
return (
<input bind={this.name} oninput={this.compare} />
)
}
}
export default Form;
💡 Binding by reference is possible because all binds are converted to the format above at transpile time.
Any object that responds to a key call with "[]" can be bound.
The name
attribute can be overwritten.
import Nullstack from 'nullstack';
class Form extends Nullstack {
number = 1;
boolean = true;
character = 'a';
text = 'aaaa';
object = {count: 1};
array = ['a', 'b', 'c'];
render({params}) {
return (
<div>
<input bind={this.number} />
<textarea bind={this.text} />
<select bind={this.character}>
{this.array.map((character) => <option>{character}</option>)}
</select>
<select bind={this.boolean} name="boolean-select">
<option value={true}>true</option>
<option value={false}>false</option>
</select>
<input bind={this.boolean} type="checkbox" />
<input bind={this.object.count} />
{this.array.map((value, index) => (
<input bind={this.array[index]} />
))}
<input bind={params.page} />
</div>
)
}
}
export default Form;
Object Events
You can use object events alongside bind
normally.
The event will run after the variable is mutated.
import Nullstack from 'nullstack';
class Paginator extends Nullstack {
render({params}) {
return (
<input bind={params.filter} oninput={{page: 1}} />
)
}
}
export default Paginator;
Simple Bindable Components
Bind can bubble down by just passing the reference from the context.
export default function CustomInput({ label, bind }) {
return (
<div>
<label> {label} </label>
<input bind={bind} />
</div>
)
}
import Nullstack from 'nullstack';
import CustomInput from './CustomInput';
class Form extends Nullstack {
name = 'Nulla';
render() {
return (
<CustomInput label="Complete Name" bind={this.name} />
)
}
}
export default Form;
Complex Bindable Components
You can create your own bindable component by receiving the attributes that bind
generates.
Bind is a transpile time shortcut that creates an object with the keys object
and property
.
class CurrencyInput extends Nullstack {
parse({event, bind}) {
const normalized = event.target.value.replace(',', '').padStart(3, '0');
const whole = (parseInt(normalized.slice(0,-2)) || 0).toString();
const decimal = normalized.slice(normalized.length - 2);
bind.object[bind.property] = parseFloat(whole+'.'+decimal);
}
render({bind}) {
const name = bind.property
const value = bind.object[bind.property]
const formatted = value.toFixed(2).replace('.', ',');
return <input name={name} value={formatted} oninput={this.parse} />
}
}
import Nullstack from 'nullstack';
import CurrencyInput from './CurrencyInput';
class Form extends Nullstack {
balance = 0;
render() {
return (
<CurrencyInput bind={this.balance} />
)
}
}
export default Form;
Debounced Bindings
You can use the attribute debounce
passing a number of miliseconds to delay setting the input value to the bound variable and the callbacks.
This is helpful when the user input could trigger heavy rerender cycles and avoids input lag.
import Nullstack from 'nullstack';
class SearchForm extends Nullstack {
term = ''
search() {
// will only run if you stop typeing for 400ms
}
render() {
return (
<input bind={this.term} debounce={400} />
)
}
}
export default SearchForm;
💡 Using the debounce attribute is more efficient than doing it manually because it uses Nullstack internal event system.