changed 5 years ago
Linked with GitHub

SPAs with web components, lit-html + redux


Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Marcus Weiner

github.com/mraerino

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Moritz Gunz

github.com/NeoLegends

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Leo Bernard

github.com/leolabs


Festify

Note:

  • Hobby project
  • Kostenlose Party App
  • Gäste können über die Musik auf deinen Partys und Veranstaltungen entscheiden

Note:

  • Basiert auf den Themen dieses Talks

Note:

  • 100% kostenlos & open-source
  • Beispiel für die Technologien, die wir heute anwenden

🏄‍

Let's dive in!

Note:

  • Wer hat schonmal mit ES6 gearbeitet?
  • Achtung: Einsatz vieler ES6 Features
    • Syntax weicht von herkömmlichem JS ab

📦

Componentization

Note:

  • Komplexität bewältigen
    • Großes Problem in mehrere kleine Probleme – Komponenten – aufteilen
  • Zusammenarbeit vereinfachen
    • Einzelne Teams oder Personen können an einzelnen Bereichen eines Projektes arbeiten
  • Seperation of Concerns
    • Komponenten tun eine Sache, und diese dafür gut
    • UNIX Prinzip
  • Abhängigkeiten vermeiden
    • Komponenten können unabhängig von anderen in mehreren Projekten eingesetzt werden
  • Verwendung in Design Systems
    • Designteams können UI Elemente wie Buttons, Inputs, etc. direkt als Komponenten gestalten -> konsistentes UI

React

class MyComponent extends React.Component { /* ... */ }

render(document.body, <MyComponent/>);

Angular

@Component({ selector: 'my-component' /* ... */ })
export class MyComponent { /* ... */ }

<my-component></my-component>

VueJS

Vue.component('my-component', { /* ... */ });

<my-component></my-component>

Note:

  • Beispiel für Komponentisierung
  • Alle grundsätzlich gleich aufgebaut
  • Komponenten sind nach außen hin eine Black Box, interne Prozesse sind nach außen hin irrelevant

🎁

Web Components


Encapsulated elements defined as JS classes

Note:
Verkapselte Elemente in Form von JS Klassen


💻

Demo


🚉

Use the platform


Runs natively in the browser:

No library code needed 🎉


True style encapsulation


Web Standard is here to stay

Note:

  • Quick edit-debug-reload cycle
  • No library lock-in (React users must use react libraries)

🤔

Browser Support



Note:

  • Shims / Polyfills available, see ShadyDom and ShadyCSS
  • Google AMP uses Web Components

UI Views

Note:

  • Moderne Webapps brauchen View Templates

Straightforward

Native

Fast

Note:

  • Wichtig: Keine exotische Syntax
  • Wie kann man nah an der Plattform bleiben?

🔥

lit-html

Note:

  • 2017, Justin Fagnani
  • Google, Polymer Project
  • Wer kennt das?

import { html, render } from 'lit-html';

const template = ({ title, greeting }) => html`
    <h1>${title}</h1>
    <p>${greeting}</p>
`;

render(template({
    title: "Talking about rendering",
    greeting: "Welcome, Webworkers"
}), document.body);

📝

Template Literals

Note:

  • Javascript
  • Platform Feature
  • ES2015 / ES6

const simple = `Hello, World!`;
const multiline = `
    <ul>
        <li>One thing</li>
        <li>another thing</li>
    </ul>
`;
const withExpression = `
    Your lucky number is 
    ${Math.floor(Math.random() * 10) + 1}
`;
const tagged = tag`Hello ${somevar}!`;

LIVE

Note:

  • Ineffizient: HTML Parsing erfolgt bei jedem Aufruf
    • innerHTML

const tag = (parts: string[], ...values: any[]) => {
    console.log(parts, values);
    return values.length;
};

const interpolationsCount = tag`Hello, ${'World'}!`; // = 1

const parse = (parts, ...values) => {
    console.log(parts, values);
    return parseImpl(parts, ...values);
    // Returns DocumentFragment made from template string
};
const template = (name) =>
    parse`<h1>Hello ${name}!</h1>`;

document.body.append(template("Webworker"));

⚡️

Rendering efficiently

Note:

  • Ineffizient: HTML Parsing erfolgt bei jedem Aufruf
    • innerHTML
  • Ansatz:
    • Statische und dynamische Teile des Templates unterscheiden
  • Statische Teile cachen!

Enter:

<template>


<template id="demo">
    <h1>Hello, World!</h1>
</template>
const fragment =
    document.querySelector('#demo').content.cloneNode(true);

Note:

  • Verstecktes Markup
  • Parsing erledigt
  • Rendert beim Hinzufügen zum DOM
  • Templates klonen ist sehr effizient
  • Plattform Feature
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

const template = (title, greeting) => html`
    <h1>${title}!</h1>
    <p>${greeting}</p>
`;

Note:

  • Zurück zu lit-html
  • Was passiert?

<template>
    <h1>{{}}</h1>
    <p>{{}}</p>
</template>

Note:

  • Erstellt <template> aus statischen Teilen des Literals
  • Browser parst Markup einmal (innerHTML)
  • Dynamische Teile durch Platzhalter ersetzt

    <h1>    </h1>
    <p>    </p>

Note:

  • Rendering-Phase
  • Template klonen
  • Platzhalter-Positionen merken

    <h1>Talking about rendering</h1>
    <p>Welcome, Webworkers</p>

Note:

  • Dynamische Teile einsetzen
  • Ergebnis in DOM einsetzen

    <h1>Rendering efficiently</h1>
    <p>Like a pro</p>

Note:

  • Update-Phase
  • DOM-Elemente gleichbleibend
  • Dynamische Teile werden neu gesetzt
    • nach Dirty Check

🗂

Nested Templates


const template = (todos) => html`
    <h2>Todos</h2>
    ${todos.map(todo => html`
        <div class="todo" id="todo-${todo.id}">
            ${todo.text}
        </div>
    `)}
`;

🦄

Uniflow

Note:

  • lit-html implementiert kein Two-Way Databinding
    • vgl. Angular, Polymer
  • Würde bedeuten, spezielle Logik mit Verhalten zu bauen, das vom DOM abweicht

let inputValue = "";
const template = html`
    <input value=${inputValue}
           on-change=${e => inputValue = e.target.value}>
`;

Note:

  • Lösung: Event Listener über spezielle Syntax anfügen

🏃‍💨   💥

Benchmarks


Easy template syntax

No build tooling needed

Tiny (< 2kb)

Considerably fast


⚛️

Redux

Note:

  • Wer kennt das schon?
  • Moderne SPAs haben viel State
    • Routing, Selected Items, Spinners
  • App als Komponente ohne sonstige Patterns zu verwalten bringt schnell Komplexitätsexplosion
    • Redux weitere Vorteile
    • MVC - Massive View Controller

Note:

  • State zwischen entfernten Komponenten teilen
    • View 1 updates Model 1 -> Model 2 -> Update Propgation?
    • Two way data binding
  • Redux gibt Schranken um Updates vorhersehbarer und skalierbarer zu machen
    • Uniflow

Three Principles


Single Source of Truth

Note:

  • Appp state ein großes Objekt
    • No encapsulation
  • Normalerweise komplexe Dinge werden einfach
    • Undo / Redo
    • Clean reset
    • Hydrating State vom server in universal apps

{
    todos: {
        'todo-1': {
            id: 'todo-1',
            title: "Listen to cool web talk at sipgate",
            priority: 3,
            completed: false,
        },
        'todo-2': {
            id: 'todo-2',
            title: "Do the laundry",
            priority: 1,
            completed: true,
        },
    },
}

Immutable State

Note:

  • State kann nicht direkt verändert werden
  • Modifications durch Anfragen 'Actions'

{
    type: 'ADD_TODO',
    payload: {
        id: 'todo-3',
        title: "Buy groceries",
        priority: 2,
        completed: false,
    },
}

Note:

  • Standard-Form
    • Type & Payload
  • In sich geschlossene Beschreibung der Mutation
    • Replay & time travel
    • Serialization & Logging

Pure State Modification

(State, Action) => State
Array.prototype.reduce(reducer, inital);

Note:

  • State modification durch reine Funktionen "reducers"
  • Kopieren Daten anstelle von Modifikation
  • Vier Aufgaben

function reduce(state = { todos: {} }, action) {
    // ...









}

Note:

  • Setup via @INIT-Action

function reduce(state = { todos: {} }, action) {
    switch (action.type) {
        case 'ADD_TODO':
            // ...




        default:
            return state;
    }
}

Note:

  • Reagieren nur auf bekannte Actions

function reduce(state = { todos: {} }, action) {
    switch (action.type) {
        case 'ADD_TODO':
            const todos = {
                ...state.todos, 
                [action.payload.id]: action.payload
            };
            // ...
        default:
            return state;
    }
}

Note:

  • Transformieren nur relevanten Teil des States

function reduce(state = { todos: {} }, action) {
    switch (action.type) {
        case 'ADD_TODO':
            const todos = {
                ...state.todos, 
                [action.payload.id]: action.payload
            };
            return { ...state, todos };
        default:
            return state;
    }
}

Note:

  • Kopieren des Reststates (flat)

⚙️

The Store

Note:

  • Simple vs. in Flux / Mobx
    • Plain JS object in implementation
  • Bringt "Drei Konzepte" zusammen
    • Enthält Reducers
  • Drei Funktionen

store.getState()


store.dispatch(action)


store.subscribe(listener)


🔁

From State to View and Back

Note:

  • Redux ist view-layer agnostisch
  • Glue-libraries kombinieren View and Redux layer
    • react-redux
    • fit-html
  • Beispiele generisch und prinzip-kompatibel mit vielen Redux-libraries

Deriving Data

Note:

  • Der größte Teil der Daten im View ist sowieso derived
  • Abgeleitete Daten müssen nicht verwaltet werden
    • Weniger Komplexität im Reducer
  • Redux hat perfekte Eigenschaften fürs Deriven

Task: Render a list of Todos (yaaaay! 🙌)

<todo-list>
    #shadow-root (open)
        <h2>Todos</h2>
        <todo-item todo-id="todo-1"></todo-item>
        <todo-item todo-id="todo-3"></todo-item>

        <h3>Completed Todos</h3>
        <todo-item todo-id="todo-2"></todo-item>
</todo-list>

Note:

  • Welche Daten brauchen wir?
    • Liste von noch-offenen und abgeschlossenen Todos
  • Welche Daten haben wir bereits?
    • Liste aller Todos
  • Wo haben wir die Daten? -> im State

MapStateToProps Function

const mapStateToProps = (state) => ({
    remainingTodos: Object.values(state.todos)
        .filter(todo => !todo.completed)
        .sort((a, b) => b.priority - a.priority),
    
    completedTodos: Object.values(state.todos)
        .filter(todo => todo.completed),
});

Note:

  • Reine Funktion, die nötige View-Daten derived
    • View ist dumm, berechnet selber nix
  • Bei jedem State-Update aufgerufen
  • Wenn Performance-Probleme, dann Selector-Library mit Caching

lit-html example

const renderTodo = (todo) => html`
    <todo-item todo-id="${todo.id}"></todo-item>
`;

const render = (props) => html`
    <h2>Todos</h2>
    ${props.remainingTodos.map(renderTodo)}

    <h3>Completed Todos</h3>
    ${props.completedTodos.map(renderTodo)}
`;

Note:

  • Vorher derivede Daten zum Rendern nutzen
  • Rendern ist deklarativ

Passing Properties

const renderTodo = (todo) => html`
    <todo-item todo-id="${todo.id}"></todo-item>
`;
const mapStateToProps = (state, ownProps) => ({
    todo: state.todos[ownProps.todoId],
});

Note:

  • Jedes einzelne Todo kriegt ID durch Attribut / Property
  • Attribute bilden zweiten Parameter von mapStateToProps ("ownProps")
    • Daten können darauf basiert derived werden

Triggering Actions

Note:

  • Bisher nur Daten ausgegeben, keine State-Modification, event handling (bzw. Data Upwards flow)

Task: Render a single Todo-Item

<todo-item>
    #shadow-root (open)
        <p>Watch cool web talk at sipgate</p>
        <button>✅</button>
</todo-item>

Note:

  • Einzelnes Todo anzeigen
  • Button, der Todo abhakt
    • Button mit State verknüpfen

MapDispatchToProps Function

const mapDispatchToProps = (dispatch, ownProps) => ({
    complete: (todoId) => dispatch({ 
        type: 'COMPLETE_TODO', 
        payload: todoId,
    }),
});

Note:

  • Reine Funktion, die dispatchende Event Handler erstellt
  • Kann an ownProps angepasst werden
  • Side effects triggern durch mehrfach-dispatch
    • In Reality über Middleware

const props = {
    ...stateProps,
    ...dispatchProps,
};

Note:

  • State & Dispatch props werden für View gemerged

lit-html example

const render = (props) => html`
    <p>${props.todo.title}</p>
    <button on-click=${() => props.complete(props.todo.id)}>
        ✅
    </button>
`;

Side Effects


Middlewares

const middleware = store => next => action => {
    // ...
    
    
    
    
    
    
    
    
    
    
    
    
};

Note:

  • Wird beim erstellen des Stores registriert
  • Zugriff auf Store, jede Action und nächste Middleware

Middlewares

const middleware = store => next => action => {
    next(action); // call next middleware
    
    // ...
    
    
    
    
    
    
    
    
    
    
};

Note:

  • Andere Middlewares aufrufen (oder auch nicht)

Middlewares

const middleware = store => next => action => {
    next(action); // call next middleware
    
    if (action.type !== 'FETCH_TODOS') {
        return;
    }
    
    // ...
    
    
    
    
    
    
};

Note:

  • Auf spezifische Actions reagieren

Middlewares

const middleware = store => next => action => {
    next(action); // call next middleware
    
    if (action.type !== 'FETCH_TODOS') {
        return;
    }
    
    const dispatchTodo = todo => store.dispatch({
        type: 'ADD_TODO',
        payload: todo,
    });
    fetch(/* ... */)
        .then(resp => resp.json())
        .then(todos => todos.forEach(dispatchTodo))
};

Note:

  • Actions transformen bevor sie zum Store kommen
  • Side effects triggern
    • => Redux Saga, Thunk, etc.

Note:


🏁 Summing up

  • Change state through actions via reducers
  • Render views declaratively
  • Derive view data in mapStateToProps
  • Prepare event handlers in mapDispatchToProps

💪

fit-html

Note:

  • Glue library between lit-html, web components and redux
  • Produces web components that take state from redux and render to shadow root
  • <3kB size, including lit-html and redux
  • Code looks like previous examples

const SearchBar = (props) => html`<!-- ... -->`;

const mapStateToProps = (state) => ({
    text: state.router.query.s || '',
});
const mapDispatchToProps = {
    changeText: changeTrackSearchInput,
    eraseText: eraseTrackSearchInput,
};

customElements.define('search-bar', connect(
    mapStateToProps,
    mapDispatchToProps,
    SearchBar,
));

Note:

  • Production code von Festify

🔀

Mixin-based

Note:

  • Nur laden, was auch nötig

const Base = connect(
    mapStateToProps,
    mapDispatchToProps,
    PartyTrack,
);
const PartyTrack = withProps(Base, {
    playing: Boolean,
    trackid: String,
});

customElements.define('party-track', PartyTrack);

Note:

  • Attribute playing, trackid deklarieren für Browser

That's it.

Ask us anything!

Follow along:

Select a repo