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

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.