Frontend Masters Hard Parts Ui
Frontend Masters Hard Parts Ui
Frontend Masters Hard Parts Ui
Academic studies
Harvard University
Oxford University
2
Principles of Analytical problem solving Technical communication
Engineering
Can you break down complex challenges and Can I implement your approach just from your
develop a strategy for solving the problem explanation
3
Two goals to UI engineering: Display content - state/data -
so the user can ‘view’ it then let them interact with it
4
Almost every piece of technology
needs a visual ‘user interface’ (UI)
5
Just two ‘simple’ goals - (1) display
content & (2) let user change it
6
Goal 1 - Display the content
7
Solution: A list of elements in C++
app.html ▼ to add to the page - the DOM
<div>
<video src="/carpool.mp4">
<p>Love Les Mis!</p>
<p>♡ 7</p>
</div> DOM - ordered list (object) of page (document) elements we can
add to using HTML
- Division
<div>
<video src="/carpool.mp4"> - Video: /carpool.mp4
9
Adding CSSOM for styling
app.html ▼
Model of Page (DOM - C++) and formatting
- Text: What’s on your mind?
What's on your mind?
<input> - Input field
- Division
<div>
<video src="/carpool.mp4"> - Video: /carpool.mp4
- HTML + CSS
11
Goal 2: Enable user to change
content they see - but problem
Pixels ≠ Data
12
Typing in the input field
app.html ▼
Model of Page (DOM - C++) changes DOM ‘data’
- Text: What’s on your mind?
What's on your mind?
<input> - Input field
- Division
<div>
<video src="/carpool.mp4"> - Video: /carpool.mp4
However:
14
Data (and code to change it) is
app.html ▼
only available in JavaScript
1 <script src="app.js"></script>
15
Webcore gives JavaScript
app.html ▼
access to the DOM & pixels
1 <input/>
Goal 1: Display content/data to the user
2 <div></div>
3 <script src="app.js"></script> 1. Data created & stored in JS: post = "Hi!"
16
app.html ▼ Solving for Goal 2: Users can change
what they see (& underlying data) by
1 <input/>
interacting
2 <div></div>
<script src="app.js"></script>
3
User interaction (typing, clicking etc) is ‘registered’ by
DOM elements (they know when they’re tapped)
app.js
But how can this cause a change in data which is
elsewhere (in Javascript)?
1 let post = ""
2 DOM events and handler functions - lets user
3 const jsInput = document.querySelector('input')
action/input (‘events’) change data in JavaScript
4 const jsDiv = document.querySelector('div')
5 jsInput.oninput = handleInput
6 function handleInput(){
By saving a function definition to the DOM element -
7 post = jsInput.value
when the user action (‘event’) happens, the function will
8 jsDiv.textContent = post
be ‘called back’ into JS and executed automatically
9 }
10 So what?
11 jsInput.oninput = handleInput
17
app.html ▼
1 <input/>
We’ve solved for UI goals 1 & 2!
2 <div></div>
<script src="app.js"></script>
3 User actions can trigger running of JS code (!) to:
Now we can display Apps let users interact (change what they see)
But this requires changing underlying data - which is only available in JavaScript - we
content and let our get to add that data to the DOM (and do many other things) with WebCore & WebIDL
JavaScript, the DOM, Events & Handlers give data then (2) redisplay the new data - so the user sees the consequence
19
Popular paradigm for tackling the essential UI
engineering challenge - data/view consistency
20
app.html ▼
1 <input/>
app.js
1 let post = ""
2 const jsInput = document.querySelector('input')
4
jsInput.value = "What's on your mind?" // affect view!
5
6 function handleInput(){ Adding another handler - this one for clicks
7 post = jsInput.value
8 jsDiv.textContent = post // affect view! We’re changing our ‘view’ based on multiple possible user
9 } interactions. In practice there will be 1000s that can all
10 function handleclick(){
affect data/view
jsInput.value = "" // affect view!
11
}
12 We need a way to make these changes as predictable
13 jsInput.oninput = handleInput as we possibly can - how?
14 jsInput.onclick = handleclick
21
app.html ▼
1 <input/>
2 <div></div> Restrict every change to view to be via
3 <script src="app.js"></script>
(1) An update of ‘data’ and
app.js (2) A run of a single dataToView
1 let post = undefined // Data convertor function
2 const jsInput = document.querySelector('input')
4
function dataToView(){ // affect view!
5
jsInput.value =
6 post == undefined ? "What's on your mind?" : post
7 jsDiv.textContent = post
8 }
9 function handleclick(){
10 post = "" // Update data
11 dataToView() // Convert data to view - This is just one approach - but an immensely
}
12 popular one (React, Next, Vue, Angular, Svelte)
13 function handleInput(){
14 post = jsInput.value // Update data - Tees us up for semi-visual coding with a virtual
15 dataToView() // Convert data to view
}
DOM
16
17 jsInput.onclick = handleclick
jsInput.oninput = handleInput
18
19 22
dataToView()
app.html ▼
1 <input/>
2 <div></div> Added benefit that we can declare our
3 <script src="app.js"></script>
data/view relationship once and when
app.js the data updates the view will update!
1 let post = undefined // Data
2 const jsInput = document.querySelector('input')
3 const jsDiv = document.querySelector('div')
4
5 function dataToView(){ // affect view!
6 jsInput.value =
post == undefined ? "What's on your mind?" : post
7
jsDiv.textContent = post
8
}
9
10 function handleInput(){ Remember our goals of UI: Display content
11 post = jsInput.value
12
} (underlying data) as ‘view’ and enable the user to
function handleclick(){
13 post = "" change it (both the underlying data and
14 }
corresponding view of it)
15 jsInput.oninput = handleInput
16 jsInput.onclick = handleclick
- We need to run the dataToView convertor on
17
18 setInterval(dataToView, 15) repeat but we’re able to automate the progress
23
1000s of ways for users to interact in a modern app
Each piece of ‘view’ (element on page) can be clicked/typed/moused over - all of
which need to change data and view
predictability to our Better to be restricted but predictable than overly flexible and it be impossible to
identify which line of code caused our view/data to change
24
What’s tomorrow?
More flexible Virtual DOM & Functional components & State hooks, Diffing & DOM
UI Composition properties reconciliation
JavaScript is not at all visual or We’ll get visual/declarative development but We need to solve the performance crisis we
declarative - we’ll make it more so with added flexibility & reusability create by solving the first 3 hard parts
25
Most misunderstood concept in UI
development
26
app.html ▼
1 <input/> Returning to our full UI with auto
2 <div></div>
updating views (from data)
3 <script src="app.js"></script>
app.js
1 let post = "" // Data
2 const jsInput = document.querySelector('input')
3 const jsDiv = document.querySelector('div')
4
5 function dataToView(){ // affect view!
6 jsInput.value = post
mypage.html ►
String interpolation gives us
‘visual’ code mypage.js ▼
developers 7 // alternative
8
- Could we do the something similar with 9 textToDisplay = `Hello, ${name}!` // "Hello, Jo!"
our main code creating visual
elements?
29
[📝 JS + Webcore]
mypage.html ►
Could we emulate HTML with
semi-visual coding? mypage.js ▼
mypage.js ▼
31
Declarative UI as a programming paradigm
Goal: See a JS nested list representation1 of your actual DOM elements (and contents) so you can get semi-visual coding 🥲
- Via nested objects or arrays of info (as we default to in UIHP) - or functional components in React
For it to be guaranteed accurate (& not out of sync) - you need your data in JS to be the only source of truth
- You can only do a JS ‘DOM’ if you’ve set on ‘one-way data binding’2 - otherwise it’s not guaranteed to be reflective of
the actual DOM (which has been altered by the user!)
So we literally take any change to the DOM (e.g. user action like typing letters in an input field) and immediately replace it by
- (i) sending the info to ‘data’ store in JS
- (ii) have that update contents of the JS DOM3
- (iii) convert that to the DOM (view)
We have semi-visual coding - our JS DOM contents and positioning on the JS page is reflected in our actual view
- We ‘declare’ (a) what page structure we want and (b) how we want its contents to depend on our data4
- And then it appears on the page (even an empty form field is the propagation of data - our ‘empty string’)
1. This is the ‘virtual DOM’. You don’t have one by default - only objects representing access to the respective DOM elements
2. Every bit of view at all times depends on underlying JS state - not whatever a user has started typing in
3. [I need to update the schema to include this ‘JS DOM’ step
32
4. Note there’s two parts to the ‘declaration’: structure/positioning & contents
Our JavaScript code mirrors our output with our JS
DOM - we can make it flexible by mapping over it
33
Lists are central to UI mypage.html ►
development mypage.js ▼
1 const vDOM = [
We have a list of 2 arrays with their
2 ['input', name, function handle (){name = jsInput.value}],
element details - but in reality it will 3 ['div', `Hello, ${name}!`]
likely be many (49 video elements in a ]
4
zoom meeting, 100 tweets in a timeline)
34
[📝 JS + Webcore]
mypage.html ►
35
[📝 JS + Webcore]
mypage.html ►
mypage.js ▼
37
mypage.html ► [📝 JS + Webcore]
mypage.js ▼
1 let posts = ["Gez", "Gerry", "Fen", "Ursy"]; let vDOM; let elems;
38
All great and awesome but we need efficiency now
We love the vDOM for semi-visual coding - however performance is a nightmare - we need some
improvements
Hooks
We’re running this updateDOM function every 15 ms - CSS animations and smooth scrolling are at risk (and
user action handlers get blocked even!).
We could only run on data change - but now our VDOM is not ‘real’ - it’s only rendered ‘as is’ if we’ve
remembered to run the updateDOM function on every data change (including from servers etc) - which is
unlikely - so introduce a ‘state hook’
Diffing
Only some of our DOM elements need recreating from scratch - we can compare archived VDOMs and
workout what changes to actually make using an algorithm
39
mypage.html ► [📝 JS + Webcore]
mypage.js ▼
1 let name = ""; let vDOM; let elems
2
Automatic update on data
3 function updateName(value){
change without looping 4 name = value
5 } updateDOM()
6
function createVDOM (){
- Instead of directly updating name, 7
return [["input", name, function handle (e){updateName(e.target.value)}],
8
we run a function ‘updateName’ to ["div", `Hello, ${name}!`],
9
do so ["div", "Great job!"]]
10 }
40
mypage.html ► [📝 JS + Webcore]
mypage.js ▼
1 const data = {name: ''}; let vDOM; let elems
2
Turning it into something we
3 function updateData(label, value){
can use with any data 4 data[label] = value
5 } updateDOM()
6
function createVDOM (){
- We can make our updatename 7
return [["input", data.name, handle],
8
function an updateData function ["div", `Hello, ${data.name}!`],
9
and have it work for any piece of ["div", "Great job!"]]
10 }
data in our app - we just hook into 11
it function handle (e){updateData('name', e.target.value)}
12
13
- So they call it a hook… function updateDOM() {
14
vDOM = createVDOM()
15
- We could switch to running elems = vDOM.map(convert);
16 document.body.replaceChildren(...elems);
requestAnimationFrame rather
17 }
than updateDOM directly on data 18
function convert(node){
change - so it never prioritizes over 19 const element = document.createElement(node[0])
animations (CSS etc) - truly 20 element.textContent = node[1]; element.value = node[1]
optimized wrt other priorities now 21 element.oninput = node[2]
22 return element
}
41
updateDOM()
mypage.html ► [📝 JS + Webcore]
mypage.js ▼
mypage.js ▼
1 let name = ''; let vDOM = createVDOM(); let prevVDOM; let elems
2
function createVDOM (){ return [
3
Finally - composable UI 4
["input", name, handle],
["div", `Hello, ${name}!`],
7
function handle (e){name = e.target.value}
8
9 function updateDOM() {
- Integrating this ‘diffing 10 if (elems == undefined){
11 elems = vDOM.map(convert)
algorithm’ makes our code for document.body.append(...elems)
12 }
building user interfaces else {
13
prevVDOM = [...vDOM]
‘semi-visual’ (enabling 14 vDOM = createVDOM()
composition) 15 findDiff(prevVDOM, vDOM)
}
16 }
- And yet not untenably inefficient 17
function convert(node) {
18
const element = document.createElement(node[0]);
19
element.textContent = node[1]
20
element.value = node[1]
21
element.oninput = node[2]
22
return element
23 }
24
function findDiff(prevVDOM, currentVDOM){
25
for (let i = 0; i < currentVDOM.length; i++) {
26 if(JSON.stringify(prevVDOM[i]) !== JSON.stringify(currentVDOM[i])){ 43
elems[i].textContent = currentVDOM[i][1]
Single source of truth for our data
Everything the user sees stems from the data. It’s restrictive but desperately
predictable - and enables data propagation to the view
We have a A JavaScript DOM that enables a semi visual form of coding our UIs - where units of code that
describe units of view can be positioned in the code to indicate their order on the webpage
groundbreaking
approach to UI
Makes reasoning about state/view much easier
The downside of semi-visual code is it takes the data and info on the view at this moment
and creates it in full (that’s why it’s so easy to reason about - we don’t have to figure out the
change from the previous data/view - just current data generating current view).
But requires techniques like hooks & VDOM diffing for efficiency
If we rebuilt the DOM itself every time any data changed that would be entirely unnecessary.
Instead we can spot the actual differences and only change those (DOM reconciliation)
44
UIHP ARCHIVE
Slide 12/13
Slide 5 Slide 7
Slide 8
Slide 9
Slide 13
📊Slide 12/13
46
Slide 14
📊Slide 14
47
Slide 26
48
mypage.html ► [📝 JS + Webcore]
mypage.js ▼
Part 1 - Event delegation 1 let name = ''; let vDOM; let elems
2
Now what if we wanted more function createVDOM(){
3
control over placement - 4
return [['div', [
['p', "Gez", function handle1 (e){name = e.target.textContent}],
nested elements 5
['p', "Ginger", function handle2 (e){name = e.target.textContent}],
6
["p", `${name}!`, function handle3 (e){name = e.target.textContent}]]]]
- We can use ‘sub-arrays’ to visually 7 }
8
show on page of code that we function updateDOM() {
9
want these ‘p’ elements to be the vDOM = createVDOM()
10
content of the ‘div’ on our elems = vDOM.map(convertNested);
11 document.body.replaceChildren(...elems);
webpage itself 12 }
13
- Our code layout determines our function convertNested(node){
14
const element = document.createElement(node[0])
actual layout (semi-visual coding!) 15 if (node[1] instanceof Array){
16 const childElems = node[1].map(convertNested);
- We have to expand our
17 element.append(...childElems)
`createDOMElement` function to 18 }
mypage.js ▼
1 let name = ''; let vDOM; let elems
2
Introducing event delegation function createVDOM(){
3
for performance/efficiency 4
return [['div', [
['p', "Gez"],
5
['p', "Ginger"],
6
['p', `${name}!`]],
- In practice we’ll have 20, 50, 100+ 7
function handle (e){name = e.target.textContent}]]
8 }
elements each needing their own
9
event handler function - totally
10 function updateDOM() {
inefficient 11 vDOM = createVDOM()
elems = vDOM.map(convertNested);
CUT 12
- Fortunately the DOM Event feature document.body.replaceChildren(...elems);
13 }
also includes ‘event bubbling’ - the
14
event floats ‘up’ and each parent function convertNested(node){
15
on the DOM tree gets hit with the const element = document.createElement(node[0])
16
if (node[1] instanceof Array){
event - and if it has a handler 17
const childElems = node[1].map(convertNested);
function that function will run in 18
element.append(...childElems)
19 }
JavaScript
20 else { element.textContent = node[1] }
21 element.onclick = node[2]
22 return element
}
23
50
24 setInterval(updateDOM, 15)
mypage.html ► [📝 JS + Webcore]
mypage.js ▼
6
- Each element on the page has a 7 function setColor(el, data) { if (data == "Will") {el.style.color = "lightgreen" } }
8
chance to ‘do’ something in the function updateDOM() {
9
user’s eyes vDOM = createVDOM()
10
elems = vDOM.map(convert)
- In reality that ‘doing’ is 11
document.body.replaceChildren(...elems)
12 }
happening in JavaScript (e.g.
13
checking a conditional, a loop function convert([type, contents, handler, fn]){
14
etc) and then some updating of const element = document.createElement(type)
15
element.textContent = contents
the view (DOM) 16
element.value = contents
17
element.oninput = handler
- We can make our vDOM 18
if (fn) { fn(element, name) }
elements store the functionality 19 return element
20 }
21
22 setInterval(updateDOM,15)
51
Could we augment our VDOM elements to include
additional functionality?
Couple of interesting ways we could try:
2. Create functions that take in our element and add functionality to it (‘decorate’ it) then return it -
Directives
3. Create our elements with a function that returns them out but before it does, it can run code to
determine exactly what it returns out - functional components
52
mypage.html ► [📝 JS + Webcore]
mypage.js ▼
1 let name = ''; let vDOM; let elems;
2
Alternatively produce our
3 function Input(){
elements with functions 4}
return ['input', name, function handle (e){name = e.target.value}]
5
function Notifcation(){
6
let className = 'darkred'
- Let’s us add logic/code/ 7
if (name == "Will"){className = 'lightgreen'}
8 return ["div", `Correct ${name}!`, , className]
functionality before we return out }
9
the VDOM element
10 function updateDOM() {
53