<style> img { border: none !important; width: 90vh; object-fit: contain; background: none !important; box-shadow: none !important; } .reveal .slides>section, .reveal .slides>section>section { padding: 0; } .reveal pre code { max-height: none; } .slides code { background: rgba(255, 255, 255, 0.1); border-radius: 0.4vh; padding: 0.5vh 1vh; color: #EF9A9A; } .slides pre code { color: white; } .hljs { color: #a9b7c6; background: #282b2e; display: block; overflow-x: auto; padding: 0.5em; } .hljs-number, .hljs-literal, .hljs-symbol, .hljs-bullet { color: #6897BB; } .hljs-keyword, .hljs-selector-tag, .hljs-deletion { color: #cc7832; } .hljs-variable, .hljs-template-variable, .hljs-link { color: #629755; } .hljs-comment, .hljs-quote { color: #808080; } .hljs-meta { color: #bbb529; } .hljs-string, .hljs-attribute, .hljs-addition { color: #6A8759; } .hljs-section, .hljs-title, .hljs-type { color: #ffc66d; } .hljs-name, .hljs-selector-id, .hljs-selector-class { color: #e8bf6a; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .profilepics > div { display: flex; justify-content: space-around; } .profilepics img { border-radius: 50%; height: 25vh; width: 25vh; object-fit: cover; max-width: 100%; } .profilepics h2 { font-size: 3vh; margin-bottom: 0; } .profilepics h3 { font-size: 2vh; } </style> # SPAs with web components, lit-html + redux --- <!-- .slide: class="profilepics" --> <div class="image"> <img src="https://i.imgur.com/WIwJk9U.jpg" width="300" height="300"> <h2>Marcus Weiner</h2> <h3>github.com/mraerino</h3> </div> <div class="image"> <img src="https://i.imgur.com/vE8DKi4.jpg" width="300" height="300"> <h2>Moritz Gunz</h2> <h3>github.com/NeoLegends</h3> </div> <div class="image"> <img src="https://i.imgur.com/PHtIbl3.jpg" width="300" height="300"> <h2>Leo Bernard</h2> <h3>github.com/leolabs</h3> </div> --- ![](https://i.imgur.com/z47N11b.png =200x200) ## Festify Note: - Hobby project - Kostenlose Party App - Gäste können über die Musik auf deinen Partys und Veranstaltungen entscheiden --- ![](https://i.imgur.com/7kT8Xz3.png) Note: - Basiert auf den Themen dieses Talks --- ![](https://i.imgur.com/tljTDgy.png) 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** ```jsx class MyComponent extends React.Component { /* ... */ } render(document.body, <MyComponent/>); ``` **Angular** ```jsx @Component({ selector: 'my-component' /* ... */ }) export class MyComponent { /* ... */ } <my-component></my-component> ``` **VueJS** ```jsx 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 --- ![](https://i.imgur.com/2XgodDT.jpg) ![](https://i.imgur.com/3ut1Ee4.png) Note: - Shims / Polyfills available, see ShadyDom and ShadyCSS - Google AMP uses Web Components --- # ✨ ## UI Views Note: - Moderne Webapps brauchen View Templates --- ## Straightforward <!-- .element: class="fragment" data-fragment-index="1" --> ## Native <!-- .element: class="fragment" data-fragment-index="2" --> ## Fast <!-- .element: class="fragment" data-fragment-index="3" --> 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?* --- ```javascript 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 --- ```javascript const simple = `Hello, World!`; ``` ```javascript const multiline = ` <ul> <li>One thing</li> <li>another thing</li> </ul> `; ``` <!-- .element: class="fragment" data-fragment-index="1" --> ```javascript const withExpression = ` Your lucky number is ${Math.floor(Math.random() * 10) + 1} `; ``` <!-- .element: class="fragment" data-fragment-index="2" --> ```javascript const tagged = tag`Hello ${somevar}!`; ``` <!-- .element: class="fragment" data-fragment-index="" --> --- # `LIVE` Note: - Ineffizient: HTML Parsing erfolgt bei jedem Aufruf - `innerHTML` ---- ```javascript const tag = (parts: string[], ...values: any[]) => { console.log(parts, values); return values.length; }; const interpolationsCount = tag`Hello, ${'World'}!`; // = 1 ``` ---- ```javascript const parse = (parts, ...values) => { console.log(parts, values); return parseImpl(parts, ...values); // Returns DocumentFragment made from template string }; ``` ```javascript 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>` --- ```html <template id="demo"> <h1>Hello, World!</h1> </template> ``` ```javascript const fragment = document.querySelector('#demo').content.cloneNode(true); ``` <!-- .element: class="fragment" data-fragment-index="" --> Note: - Verstecktes Markup - Parsing erledigt - Rendert beim Hinzufügen zum DOM - Templates klonen ist sehr effizient - Plattform Feature :tada: --- ```javascript const template = (title, greeting) => html` <h1>${title}!</h1> <p>${greeting}</p> `; ``` Note: - Zurück zu `lit-html` - *Was passiert?* --- ```html <template> <h1>{{}}</h1> <p>{{}}</p> </template> ``` Note: - Erstellt `<template>` aus statischen Teilen des Literals - Browser parst Markup **einmal** (`innerHTML`) - Dynamische Teile durch Platzhalter ersetzt --- ```html <h1> </h1> <p> </p> ``` Note: - Rendering-Phase - Template klonen - Platzhalter-Positionen merken --- ```html <h1>Talking about rendering</h1> <p>Welcome, Webworkers</p> ``` Note: - Dynamische Teile einsetzen - Ergebnis in DOM einsetzen --- ```html <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 --- ```javascript 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 --- ```javascript 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 --- ## 🏃‍💨&nbsp;&nbsp;&nbsp;💥 ## Benchmarks ![](https://raw.githubusercontent.com/vogloblinsky/web-components-benchmark/master/screenshots/benchmark_edit.png) <!-- .element: class="fragment" data-fragment-index="1" --> --- ## Easy template syntax <!-- .element: class="fragment" data-fragment-index="1" --> ## No build tooling needed <!-- .element: class="fragment" data-fragment-index="2" --> ## Tiny (< 2kb) <!-- .element: class="fragment" data-fragment-index="3" --> ## Considerably fast <!-- .element: class="fragment" data-fragment-index="4" --> --- # ⚛️ ## 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 --- ![](https://i.imgur.com/6bWCmX8.png) 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 --- ```javascript { 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' --- ```javascript { 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 ```javascript (State, Action) => State ``` <!-- .element: class="fragment" data-fragment-index="1" --> ```javascript Array.prototype.reduce(reducer, inital); ``` <!-- .element: class="fragment" data-fragment-index="2" --> Note: - State modification durch reine Funktionen "reducers" - Kopieren Daten anstelle von Modifikation - Vier Aufgaben --- ```javascript function reduce(state = { todos: {} }, action) { // ... } ``` Note: - Setup via @INIT-Action --- ```javascript function reduce(state = { todos: {} }, action) { switch (action.type) { case 'ADD_TODO': // ... default: return state; } } ``` Note: - Reagieren nur auf bekannte Actions --- ```javascript 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 --- ```javascript 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! 🙌) ```html <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 ```javascript 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 ```javascript 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 ```javascript const renderTodo = (todo) => html` <todo-item todo-id="${todo.id}"></todo-item> `; ``` ```javascript const mapStateToProps = (state, ownProps) => ({ todo: state.todos[ownProps.todoId], }); ``` <!-- .element: class="fragment" data-fragment-index="1" --> 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 ```html <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 ```javascript 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 --- ```javascript const props = { ...stateProps, ...dispatchProps, }; ``` Note: - State & Dispatch props werden für View gemerged --- lit-html example ```javascript const render = (props) => html` <p>${props.todo.title}</p> <button on-click=${() => props.complete(props.todo.id)}> ✅ </button> `; ``` --- # ⛓ ## Side Effects --- Middlewares ```javascript const middleware = store => next => action => { // ... }; ``` Note: - Wird beim erstellen des Stores registriert - Zugriff auf Store, jede Action und nächste Middleware --- Middlewares ```javascript const middleware = store => next => action => { next(action); // call next middleware // ... }; ``` Note: - Andere Middlewares aufrufen (oder auch nicht) --- Middlewares ```javascript const middleware = store => next => action => { next(action); // call next middleware if (action.type !== 'FETCH_TODOS') { return; } // ... }; ``` Note: - Auf spezifische Actions reagieren --- Middlewares ```javascript 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. --- ![](https://i.imgur.com/KwqYEUs.gif) Note: - Uniflow erkenntlich - Taken from http://slides.com/jenyaterpil/redux-from-twitter-hype-to-production#/ --- 🏁 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 --- ```javascript 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 --- ```javascript 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:** - github.com/Festify/fit-html - npmjs.com/package/fit-html - twitter.com/GetFestify
{"metaMigratedAt":"2023-06-15T03:03:24.374Z","metaMigratedFrom":"YAML","title":"SPAs with web components, lit-html + redux","breaks":true,"slideOptions":"{\"theme\":\"dark\",\"slideNumber\":false,\"controls\":false,\"transition\":\"fade\"}","contributors":"[{\"id\":\"53c655a0-3881-4259-afc8-68b5fcfce041\",\"add\":18909,\"del\":0}]"}
    1268 views