Props Down, Events Up; A Guide to Managing State
Me
Matthew Phillips
Twitter: @matthewcp
Social
GitHub: matthewp
Bram
The best web component library.
Matthew Phillips
State
the condition of matter with respect to structure, form, constitution,phase, or the like
Types of state
- Application
- Component
- Session
Starting with state
Todos
/todos
State
{
"page": "todos",
"todos": [
"walk the dog",
"go to the groceries"
]
}
State patterns
MVC
MVVM
PubSub
Flux
Interceptor
Singleton
MVP
Naked Objects
Observables
MVVM
ViewModel
firstName
lastName
fullName
Other
ViewModels
Benefits to Observables 💃
- Computed properties make it easy to define their own dependencies.
- Makes it easy to connect disparate parts of an application.
Pitfalls of Observables
- Can cause deep event chains.
- Can be difficult to debug and find the source of a change.
- Changes in child components can cause changes to parents.
Unidirectional
Flux
View
Actions
State
Reducers
events
Parent
Child
Properties & Attributes
Events
Props + Events
Being a good web component citizen
Build APIs others easily understand
Component categories
shared vs. project
<my-tabs>
<my-panel>First</my-panel>
<my-panel>Second</my-panel>
</my-tabs>
<shopping-cart></shopping-cart>
Do as the web does
- Allow configuration through attributes and properties.
- Inform parent elements of changes through events.
Example: <img>
Learning from native elements
<img> lets you define a src attribute/property
And dispatches load and error events
var img = new Image();
img.addEventListener('load', _ => {
console.log('Image loaded');
});
img.addEventListener('error', _ => {
console.error('Failed to load image');
});
// Setting the property triggers a fetch
img.src = 'https://bramjs.org/images/bram.svg';
<img>
class MyImage extends HTMLElement {
get src() {
return this._src;
}
set src(val) {
this._src = val;
this._fetch();
}
_fetch() {
fetch(this.src)
.then(_ => new Event('load'))
.catch(_ => new Event('error'))
.then(ev => {
this.dispatchEvent(ev);
});
}
}
<my-img>
Example: <progress>
Another awesome native element!
<progress> has a value attribute
and a value property
<progress value="0.5"></progress>
HTML
let progress = document.querySelector('progress');
progress.value = '0.3';
JavaScript
<progress> has a max attribute
and a max property
<progress value="20" max="100"></progress>
HTML
let progress = document.querySelector('progress');
progress.max = 30;
JavaScript
How can we this?
It's not easy
class MyProgress extends HTMLElement {
static get observedAttributes() {
return ['value', 'max'];
}
constructor() {
super();
this._value = this.getAttribute('value');
this._max = this.getAttribute('max');
}
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.setAttribute('value', val);
}
get max() {
return this._max;
}
set max(val) {
this._max = val;
}
}
customElements.define('my-progress', MyProgress);
Supporting Attribute/Property pairs
static get observedAttributes() {
return ['value', 'max'];
}
observedAttributes
progress.setAttribute('value', '0.4');
makes it so that...
calls attributedChangedCallback
attributeChangedCallback(name, oldVal, newVal) {
// what do?
}
attributeChangedCallback(name, oldVal, newVal) {
this[name] = newVal;
}
attributeChangedCallback
Will call your setter with the new value
set max(val) {
// ok now what?
}
Property setter
set max(val) {
this._max = val;
this.setAttribute('max', val);
}
Needs to call setAttribute
Constructor
constructor() {
super();
this._max = this.getAttribute('max');
}
Sets up initial values
To Think About 🤔
Should setting a property set the attribute if the attribute doesn't already exist?
<progress value="0.8"></progress>
HTML
JavaScript
let progress = document.querySelector('progress');
progress.max = 2;
is an attribute added?
<progress value="0.8" max="2"></progress>
HTML
Choosing the right tool 🔨
When to use attributes, properties, and events
Attributes
<my-accordion orientation="landscape">
<my-panel>Panel one</my-panel>
<my-panel open>Panel two</my-panel>
<my-panel>Panel three</my-panel>
</my-accordion>
- Initial state
- State that should be queryable
- State that could be styled
- State represented as string/boolean
Attribute styling
<input type="text" value="Hello world" disabled>
HTML
input[disabled] {
background-color: WhiteSmoke;
}
CSS
boolean attributes provide a way to toggle behavior
attributes are important because they are native HTML
HTML is the only universal way to use your element
It can be too easy to forget about attributes when using a framework
Properties
let userForm = document.querySelector('user-form');
userForm.user = {
name: 'Matthew',
gh: 'matthewp'
};
- Complex data
- Changes to state after the element is inserted
- Even state that can't be set should be available as getters (derived state)
Changing state
<label for="user">Username:</label>
<input type="text" name="user" value="Matthew">
let input = document.querySelector('[name=user]');
input.value = 'Wilbur';
HTML
JavaScript
Exposing methods
<audio controls src="http://ia801400.us.archive.org/33/items/frankenstein_shelley/frankenstein_00_shelley_64kb.mp3">
</audio>
let audio = document.querySelector('audio');
audio.play();
HTML
JavaScript
Derived State
<time datetime="2017-04-29T19:00"></time>
HTML
var time = document.querySelector('time');
time.date.getMonth();
JavaScript
Doesn't work 😤
<my-time>
class MyTime extends HTMLElement {
set datetime(val) {
this.date = new Date(val);
this.setAttribute('datetime', val);
}
}
customElements.define('my-time', MyTime);
A better time that exposes the date object
Attribute vs Property
Attribute
Property
- Initial state
- Styling 😎
- Strings/booleans
- New state
- Can't style 😢
- Objects/Arrays
- Reveal internal state
Properties
Attributes
class MyModal extends HTMLElement {
static get observedAttributes() {
return ['open'];
}
constructor() {
super();
this._open = this.hasAttribute('open');
}
get open() {
return this._open;
}
set open(val) {
val = Boolean(val);
if(this._open !== val) {
this._open = val;
if(val) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
}
}
attributeChangedCallback(name, oldVal, newVal) {
this.open = newVal === "";
}
}
Bidirectional Attribute + Property
Events
let modal = document.querySelector('modal-window');
modal.addEventListener('close', e => {
// Do something when this closes
});
- Reflects changes to the element's internal state.
- User initiated.
class EveryFiveSeconds extends HTMLElement {
connectedCallback() {
this._id = setInterval(_ => {
this.dispatchEvent(
new CustomEvent('tick', {
detail: new Date()
})
);
}, 5000);
}
}
customElements.define('every-five-seconds', EveryFiveSeconds);
let el = document.querySelector('every-five-seconds');
el.addEventListener('tick', e => console.log('Time:', e.detail));
Include data in the event detail
relevant to the consumer
Events reproject internal events
They hide the ugly implementation details of how your element works
class UserForm extends HTMLElement {
constructor() {
super();
let root = this.attachShadow({ mode: 'open' });
renderTemplate(root);
let form = root.querySelector('form');
form.addEventListener('submit', e => {
this.dispatchEvent(
new CustomEvent('user-change', {
detail: this.user
})
);
});
}
}
customElements.define('user-form', UserForm);
Reprojecting internal events
To consider 🤔
- bubbling is false by default
- use bubbles: true
- composed: true allows events to pass through shadow boundaries
onevent
let modal = document.querySelector('modal-window');
modal.onclose = () => alert('Modal closed');
let el = document.querySelector('a');
el.onclick = e => {
e.preventDefault();
// do stuff
};
Built-in elements
Custom elements
<div id="parent">
<input type="text" name="user" value="">
</div>
<script>
parent.onchange = function(){
console.log("Something changed.");
}
</script>
Delegation
class ModalWindow extends HTMLElement {
get onclose() {
return this._onclose;
}
set onclose(handler) {
if(this._onclose) {
this.removeEventListener('close', this._onclose);
}
this._onclose = handler;
this.addEventListener('close', this._onclose);
}
connectedCallback() {
if(this._onclose)
this.addEventListener('close', this._onclose);
}
disconnectedCallback() {
if(this._onclose)
this.removeEventListener('close', this._onclose);
}
}
customElements.define('modal-window', ModalWindow);
Implementing onevent
drawbacks
-
Verbose to implement 😵
-
Libraries can help
-
-
Do not work on other elements
No bubbling
benefits
Convenient
Interoperability (kind of? maybe?)
Takeaways
Many awesome state patterns
Be a good web component citizen
-
Properties and attributes go down
Attributes are a subset of properties
-
Events come back up
The more events the merrier
-
onevent for convenience
maybe not worth the hassle
THE END
BY MATTHEW PHILLIPS
Follow me.
Props Down, Events Up; A Guide to Managing State
By Matthew Phillips
Props Down, Events Up; A Guide to Managing State
- 664