Stop Writing Javascript Frameworks
Joe Gregorio@bitworking
bit.ly/zf-preso-2
● I am employed by Google
● This talk does not represent the opinion of Google
Caveats
● Code is for exposition purposes only.
Caveats
● Those expecting magic and/or cleverness.
Apologies in Advance
● Chrome● Android● Firefox● Flutter.io● Sublime Text 3● ...
About Skia
● Hidden Gems○ PathOps
https://skia.org/dev/present/pathops ○ SkNx https://skia.org/dev/contrib/simd
About Skia
● Infrastructure○ Run the Tests○ Tools for Analysis
■ 500,000 Images/Commit■ 260,000 Perf Metrics/Commit
About Me
● Go● Polymer● Google Compute Engine● Open Source
○ https://skia.googlesource.com/buildbot/
About Skia Infra
● Build fast● Iterate quickly● Maintain robustly● Do things at scale
Context
Stop Writing Javascript Frameworks
http://bitworking.org/news/2014/05/zero_framework_manifesto
using hyperbole
https://xkcd.com/386/
Slightly Hyperbolic Title
Things I didn't say
● Stop innovating.
● Stop using Javascript libraries.
● Stop using tools.
● I have all the answers.
Alternate Accurate Titles
● Stop Using Javascript Frameworks
● Start writing reusable orthogonally
composable units of HTML+CSS+JS
Stop Writing Using Javascript Frameworks
Joe Gregorio@bitworking
What do I mean by "framework"?
gist -> library -> framework
gist -> library -> framework
Trouble starts here
● HTML
● CSS
● JS
● HTML
● CSS
● JS
GWT
● HTML
● CSS
● JS
There was a time for frameworks
Remember the IE box model?
https://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
Remember the IE event model?
https://en.wikipedia.org/wiki/DOM_events#Microsoft-specific_model
Remember the Netscape layer tag?
<LAYER ID=layer1 TOP=20pt LEFT=5pt BGCOLOR="#CC00EE" WIDTH=200> <H1>Layer 1</H1> <P>Lots of content for this layer.</P> <IMG SRC=violets.jpg align=right> <P>Content for layer 1.</P> <P>More Content for layer 1.</P></LAYER>
http://web.archive.org/web/20040929103224/http://developer.netscape.com/docs/manuals/communicator/dynhtml/index.htm
There was no consistent model for interacting with the browser.
The situation has improved
So that weakens one reason to use a framework.
Polyfills
The "import __future__" for the web.
Framework
Polyfills
Fill in the potholes.
But what about all the other benefits a framework brings?
Abstractions are Leaky
Abstractions are Abstract
https://upload.wikimedia.org/wikipedia/commons/a/ac/Iceberg.jpg
Reuse
Busy porting my Dojo widgets to Backbone.
Angular 1.0 -> 2.0
Framework
Polyfills
Fill in the potholes.
Code organization
?
Framework
Polyfills Web Components
Fill in the potholes.
Code organization
Web Components
● HTML Templates● HTML Imports● Custom Elements● Shadow DOM
Web Components
● HTML Templates● HTML Imports● Custom Elements● Shadow DOM
HTML Templates
● Now part of HTML spec
HTML Templates
HTML Templates
● <template>...</template>● Content is parsed by browser.● Content is not rendered (invisible to user).● Content is hidden (invisible to
querySelector).● Content is inert. (JS doesn't run)
<template id=t> <div>Hello World!</div></template>
<script> var node = document.importNode( document.querySelector('#t').content, true); document.body.appendChild(node);</script>
<template id=t> <div>Hello World!</div></template>
<script> var node = document.importNode( document.querySelector('#t').content, true); document.body.appendChild(node);</script>
<template id=t> <div>Hello World!</div></template>
<script> var node = document.importNode( document.querySelector('#t').content, true); document.body.appendChild(node);</script>
<template id=t> <div>Hello World!</div></template>
<script> var node = document.importNode( document.querySelector('#t').content, true); document.body.appendChild(node);</script>
<template id=t> <div>Hello World!</div></template>
<script> var node = document.importNode( document.querySelector('#t').content, true); document.body.appendChild(node);</script>
bit.ly/zf-template
HTML Imports● <link rel="import" href="">
● Oddly slightly contentious.
● Brilliant for code organization, keep HTML, CSS, and JS together in one file.
HTML Imports● Are de-dup'd
● Can contain anything an HTML file can contain (HTML, CSS, JS)
● Scripts are executed in the context of the window that imported them
<template id=t> <div> Hello World! </div></template>
<script> (function() { var doc = document.currentScript.ownerDocument; var node = document.importNode( doc.querySelector( '#t').content, true); document.body.appendChild(node); })();</script>
import.html
<template id=t> <div> Hello World! </div></template>
<script> (function() { var doc = document.currentScript.ownerDocument; var node = document.importNode( doc.querySelector('#t').content, true); document.body.appendChild(node); })();</script>
import.html
<template id=t> <div> Hello World! </div></template>
<script> (function() { var doc = document.currentScript.ownerDocument; var node = document.importNode( doc.querySelector('#t').content, true); document.body.appendChild(node); })();</script>
import.html
<template id=t> <div> Hello World! </div></template>
<script> (function() { var doc = document.currentScript.ownerDocument; var node = document.importNode( doc.querySelector('#t').content, true); document.body.appendChild(node); })();</script>
import.html
<template id=t> <div> Hello World! </div></template>
<script> (function() { var doc = document.currentScript.ownerDocument; var node = document.importNode( doc.querySelector('#t').content, true); document.body.appendChild(node); })();</script>
import.html
<html> <head> <script src="webcomponents.js"></script> <link rel="import" href="import.html"> </head> <body> </body></html>
bit.ly/zf-html-imports-tmpl
index.html
<html> <head> <script src="webcomponents.js"></script> <link rel="import" href="import.html"> </head> <body> </body></html>
bit.ly/zf-html-imports-tmpl
index.html
http://webcomponents.org
<html> <head> <script src="webcomponents-lite.js"></script> <link rel="import" href="import.html"> </head> <body> </body></html>
bit.ly/zf-html-imports-tmpl
index.html
<html> <head> <script src="webcomponents.js"></script> <link rel="import" href="import.html"> </head> <body> </body></html>
bit.ly/zf-html-imports-tmpl
index.html
Context = function() { this.doc = (document.currentScript||document._currentScript).ownerDocument;};
Context.prototype. import = function(id) { return document.importNode(this.doc.querySelector( '#'+id).content, true);};
util.js
<template id=t> <div class=greeting> Hello World! </div></template>
<script> (function() { var ctx = new Context(); document.body.appendChild(ctx. import('t')); })();</script>
bit.ly/zf-import-context
import.html
Custom Elements
● Oddly uncontentious
● Only talking about creating new elements, not subclassing existing elements.
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() { this.textContent = 'Hello World!';};
proto.addMoreText = function(s) { this.textContent = this.textContent + s;};
document.registerElement('hello-element', { prototype: proto});
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() { this.textContent = 'Hello World!';};
proto.addMoreText = function(s) { this.textContent = this.textContent + s;};
document.registerElement('hello-element', { prototype: proto});
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() { this.textContent = 'Hello World!';};
proto.addMoreText = function(s) { this.textContent = this.textContent + s;};
document.registerElement('hello-element', { prototype: proto});
Custom Element Lifecycle Callbacks
createdCallback
attachedCallback
detachedCallback
attributeChangedCallback
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() { this.textContent = 'Hello World!';};
proto.addMoreText = function(s) { this.textContent = this.textContent + s;};
document.registerElement('hello-element', { prototype: proto});
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() { this.textContent = 'Hello World!';};
proto.addMoreText = function(s) { this.textContent = this.textContent + s;};
document.registerElement('hello-element', { prototype: proto});
<html><body>
<hello-element></hello-element></body></html>
bit.ly/zf-custom-element
Context = function() { this.doc = (document.currentScript||document._currentScript).ownerDocument;};
Context.prototype. import = function(id) { return document.importNode(this.doc.querySelector( '#'+id).content, true);};
function newElement(name, proto) { var ep = Object.create(HTMLElement.prototype); Object.keys(proto).forEach( function(key) { ep[key] = proto[key]; }); document.registerElement(name, {prototype : ep});}
util.js
Context = function() { this.doc = (document.currentScript||document._currentScript).ownerDocument;};
Context.prototype. import = function(id) { return document.importNode( this.doc.querySelector('#'+id).content, true);};
function newElement(name, proto) { var ep = Object.create(HTMLElement.prototype); Object.keys(proto).forEach(function(key) { ep[key] = proto[key]; }); document.registerElement(name, {prototype: ep});}
util.js
newElement('hello-element', {
createdCallback: function() { this.textContent = 'Hello World!'; },
addMoreText: function(s) { this.textContent = this.textContent + s; },});
bit.ly/zf-newelement
Putting it all together
A motivating example
<template id=t> <button>Hide</button></template>
<script> (function() { var ctx = new Context();
newElement('toggle-next', { createdCallback: function() { this.appendChild(ctx.import('t')); this.button = this.querySelector('button'); this.button.addEventListener('click', this._buttonClick.bind(this)); },
_buttonClick: function() { var current = this.nextElementSibling.style.visibility; if (current == 'collapse') { this.nextElementSibling.style.visibility = 'visible'; this.button.innerText = 'Hide'; } else { this.nextElementSibling.style.visibility = 'collapse'; this.button.innerText = 'Show'; } } }) })();</script>
<template id=t> <button>Hide</button></template>
<script> (function() { var ctx = new Context();
newElement('toggle-next', { createdCallback: function() { this.appendChild(ctx.import('t')); this.button = this.querySelector('button'); this.button.addEventListener('click', this._buttonClick.bind(this)); },
_buttonClick: function() { var current = this.nextElementSibling.style.visibility; if (current == 'collapse') { this.nextElementSibling.style.visibility = 'visible'; this.button.innerText = 'Hide'; } else { this.nextElementSibling.style.visibility = 'collapse'; this.button.innerText = 'Show'; } } }) })();</script>
<template id=t> <button>Hide</button></template>
<script> (function() { var ctx = new Context();
newElement('toggle-next', { createdCallback: function() { this.appendChild(ctx.import('t')); this.button = this.querySelector('button'); this.button.addEventListener('click', this._buttonClick.bind(this)); },
_buttonClick: function() { var current = this.nextElementSibling.style.visibility; if (current == 'collapse') { this.nextElementSibling.style.visibility = 'visible'; this.button.innerText = 'Hide'; } else { this.nextElementSibling.style.visibility = 'collapse'; this.button.innerText = 'Show'; } } }) })();</script>
<template id=t> <button>Hide</button></template>
<script> (function() { var ctx = new Context();
newElement('toggle-next', { createdCallback: function() { this.appendChild(ctx.import('t')); this.button = this.querySelector('button'); this.button.addEventListener('click', this._buttonClick.bind(this)); },
_buttonClick: function() { var current = this.nextElementSibling.style.visibility; if (current == 'collapse') { this.nextElementSibling.style.visibility = 'visible'; this.button.innerText = 'Hide'; } else { this.nextElementSibling.style.visibility = 'collapse'; this.button.innerText = 'Show'; } } }) })();</script>
<template id=t> <button>Hide</button></template>
<script> (function() { var ctx = new Context();
newElement('toggle-next', { createdCallback: function() { this.appendChild(ctx.import('t')); this.button = this.querySelector('button'); this.button.addEventListener('click', this._buttonClick.bind(this)); },
_buttonClick: function() { var current = this.nextElementSibling.style.visibility; if (current == 'collapse') { this.nextElementSibling.style.visibility = 'visible'; this.button.innerText = 'Hide'; } else { this.nextElementSibling.style.visibility = 'collapse'; this.button.innerText = 'Show'; } } }) })();</script>
<template id=t> <button>Hide</button></template>
<script> (function() { var ctx = new Context();
NewElement('toggle-next', { createdCallback: function() { this.appendChild(ctx.import('t')); this.button = this.querySelector('button'); this.button.addEventListener('click', this._buttonClick.bind(this)); },
_buttonClick: function() { var current = this.nextElementSibling.style.visibility; if (current == 'collapse') { this.nextElementSibling.style.visibility = 'visible'; this.button.innerText = 'Hide'; } else { this.nextElementSibling.style.visibility = 'collapse'; this.button.innerText = 'Show'; } } }) })();</script>
<template id=t> <button>Hide</button></template>
<script> (function() { var ctx = new Context();
newElement('toggle-next', { createdCallback: function() { this.appendChild(ctx.import('t')); this.button = this.querySelector('button'); this.button.addEventListener('click', this._buttonClick.bind(this)); },
_buttonClick: function() { var current = this.nextElementSibling.style.visibility; if (current == 'collapse') { this.nextElementSibling.style.visibility = 'visible'; this.button.innerText = 'Hide'; } else { this.nextElementSibling.style.visibility = 'collapse'; this.button.innerText = 'Show'; } } }) })();</script>
<!DOCTYPE html><html>
<head> <script src="webcomponents.js"></script> <script src="util.js"></script> <link rel="import" href="toggle.html"></head>
<body> <toggle-next></toggle-next> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dignissim, nisi non rhoncus condimentum, felis ante porta eros, ut tincidunt odio nisl a libero. </p></body>
</html>
<!DOCTYPE html><html>
<head> <script src="webcomponents.js"></script> <script src="util.js"></script> <link rel="import" href="toggle.html"></head>
<body> <toggle-next></toggle-next> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dignissim, nisi non rhoncus condimentum, felis ante porta eros, ut tincidunt odio nisl a libero. </p></body>
</html>
<!DOCTYPE html><html>
<head> <script src="webcomponents.js"></script> <script src="util.js"></script> <link rel="import" href="toggle.html"></head>
<body> <toggle-next></toggle-next> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dignissim, nisi non rhoncus condimentum, felis ante porta eros, ut tincidunt odio nisl a libero. </p></body>
</html>
<!DOCTYPE html><html>
<head> <script src="webcomponents.js"></script> <script src="util.js"></script> <link rel="import" href="toggle.html"></head>
<body> <toggle-next></toggle-next> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dignissim, nisi non rhoncus condimentum, felis ante porta eros, ut tincidunt odio nisl a libero. </p></body>
</html>
<!DOCTYPE html><html>
<head> <script src="webcomponents.js"></script> <script src="util.js"></script> <link rel="import" href="toggle.html"></head>
<body> <toggle-next></toggle-next> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi dignissim, nisi non rhoncus condimentum, felis ante porta eros, ut tincidunt odio nisl a libero. </p></body>
</html>
bit.ly/zf-toggle
So how did this work?
Adjacency in the DOM tree.
Framework
Polyfills Web Components
Fill in the potholes.
Code organization
Organizing around the surfaces already present in the browser.
● Attributes
● Events
● DOM Structure
<button id=submit>Submit</button>
document.querySelector('#submit').id
document.querySelector('#submit').click()
Attributes
<button id=submit>Submit</button>
document.querySelector('#submit').id
document.querySelector('#submit').click()
Attributes
<button id=submit>Submit</button>
document.querySelector('#submit').id
document.querySelector('#submit').click()
Attributes
<button id=submit>Submit</button>
document.querySelector('#submit').id
document.querySelector('#submit').click()
Attributes
ele.addEventListener('some-event', function(e) { /* */ });
var e = new CustomEvent('some-event', {details: details});this.dispatchEvent(e);
Events
ele.addEventListener('some-event', function(e) { /* */ });
var e = new CustomEvent('some-event', {details: details});this.dispatchEvent(e);
Events
ele.addEventListener('some-event', function(e) { /* */ });
var e = new CustomEvent('some-event', {details: details});this.dispatchEvent(e);
Events
<button> <img src=""></button>
<div> <p class=foo></p> <p class=foo></p> <p class=foo></p> <p class=bar></p></div>
DOM Structure
<button> <img src=""> Parent/Child</button>
<div> <p class=foo></p> <p class=foo></p> <p class=foo></p> <p class=bar></p></div>
DOM Structure
<button> <img src=""></button>
<div> <p class=foo></p> <p class=foo></p> CSS Selectors <p class=foo></p> querySelector('.foo') <p class=bar></p></div>
DOM Structure
A more detailed example
<sort-stuff data-target="#sortable"> <button data-key=pop>Sort on Population</button> <button data-key=density>Sort on Density</button> <button data-key=change>Sort on % Change</button></sort-stuff>
<ul id=sortable> <li data-pop=8.4 data-density=27000 data-change=3.86>New York</li> <li data-pop=3.9 data-density=8092 data-change=3.59>Los Angeles</li> <li data-pop=2.7 data-density=11842 data-change=0.99>Chicago</li> <li data-pop=2.2 data-density=3501 data-change=6.63>Houston</li> <li data-pop=1.5 data-density=11379 data-change=2.25>Philadelphia</li></ul>
<sort-stuff data-target="#sortable"> <button data-key=pop>Sort on Population</button> <button data-key=density>Sort on Density</button> <button data-key=change>Sort on % Change</button></sort-stuff>
<ul id=sortable> <li data-pop=8.4 data-density=27000 data-change=3.86>New York</li> <li data-pop=3.9 data-density=8092 data-change=3.59>Los Angeles</li> <li data-pop=2.7 data-density=11842 data-change=0.99>Chicago</li> <li data-pop=2.2 data-density=3501 data-change=6.63>Houston</li> <li data-pop=1.5 data-density=11379 data-change=2.25>Philadelphia</li></ul>
<sort-stuff data-target="#sortable"> <button data-key=pop>Sort on Population</button> <button data-key=density>Sort on Density</button> <button data-key=change>Sort on % Change</button></sort-stuff>
<ul id=sortable> <li data-pop=8.4 data-density=27000 data-change=3.86>New York</li> <li data-pop=3.9 data-density=8092 data-change=3.59>Los Angeles</li> <li data-pop=2.7 data-density=11842 data-change=0.99>Chicago</li> <li data-pop=2.2 data-density=3501 data-change=6.63>Houston</li> <li data-pop=1.5 data-density=11379 data-change=2.25>Philadelphia</li></ul>
<sort-stuff data-target="#sortable"> <button data-key=pop>Sort on Population</button> <button data-key=density>Sort on Density</button> <button data-key=change>Sort on % Change</button></sort-stuff>
<ul id=sortable> <li data-pop=8.4 data-density=27000 data-change=3.86>New York</li> <li data-pop=3.9 data-density=8092 data-change=3.59>Los Angeles</li> <li data-pop=2.7 data-density=11842 data-change=0.99>Chicago</li> <li data-pop=2.2 data-density=3501 data-change=6.63>Houston</li> <li data-pop=1.5 data-density=11379 data-change=2.25>Philadelphia</li></ul>
<sort-stuff data-target="#sortable"> <button data-key=pop>Sort on Population</button> <button data-key=density>Sort on Density</button> <button data-key=change>Sort on % Change</button></sort-stuff>
<ul id=sortable> <li data-pop=8.4 data-density=27000 data-change=3.86>New York</li> <li data-pop=3.9 data-density=8092 data-change=3.59>Los Angeles</li> <li data-pop=2.7 data-density=11842 data-change=0.99>Chicago</li> <li data-pop=2.2 data-density=3501 data-change=6.63>Houston</li> <li data-pop=1.5 data-density=11379 data-change=2.25>Philadelphia</li></ul>
newElement('sort-stuff', { createdCallback : function() { this.addEventListener( 'click', this._clickHandler.bind( this)); },
_clickHandler : function(e) { ... }, });
newElement('sort-stuff', { createdCallback : function() { this.addEventListener( 'click', this._clickHandler.bind( this)); },
_clickHandler: function(e) { ... }, });
_clickHandler: function(e) { var key = e.target.dataset.key; if (!key) { return; } var target = document.querySelector(this.dataset.target); var elements = []; var children = target.children; for (var i=0; i<children.length; i++) { var ele = children[i]; elements.push({ value: +ele.dataset[key], node: ele, }); } elements.sort(function(x, y) { return (x.value == y.value ? 0 : (x.value < y.value ? 1 : -1)); }); elements.forEach(function(i) { target.appendChild(i.node); }); },
_clickHandler: function(e) { var key = e.target.dataset.key; if (!key) { return; } var target = document.querySelector(this.dataset.target); var elements = []; var children = target.children; for (var i=0; i<children.length; i++) { var ele = children[i]; elements.push({ value: +ele.dataset[key], node: ele, }); } elements.sort(function(x, y) { return (x.value == y.value ? 0 : (x.value < y.value ? 1 : -1)); }); elements.forEach(function(i) { target.appendChild(i.node); }); },
_clickHandler: function(e) { var key = e.target.dataset.key; if (!key) { return; } var target = document.querySelector(this.dataset.target); var elements = []; var children = target.children; for (var i=0; i<children.length; i++) { var ele = children[i]; elements.push({ value: +ele.dataset[key], node: ele, }); } elements.sort(function(x, y) { return (x.value == y.value ? 0 : (x.value < y.value ? 1 : -1)); }); elements.forEach(function(i) { target.appendChild(i.node); }); },
_clickHandler: function(e) { var key = e.target.dataset.key; if (!key) { return; } var target = document.querySelector(this.dataset.target); var elements = []; var children = target.children; for (var i=0; i<children.length; i++) { var ele = children[i]; elements.push({ value: +ele.dataset[key], node: ele, }); } elements.sort(function(x, y) { return (x.value == y.value ? 0 : (x.value < y.value ? 1 : -1)); }); elements.forEach(function(i) { target.appendChild(i.node); }); },
● sort-stuff has 'data-target' [Attribute] that selects [DOM Structure] the element
to sort.
● The target children [DOM Structure] have data attributes [Attribute] that
elements are sorted on.
● sort-stuff registers for a 'click' [Events] from its children [DOM Structure].
● sort-stuff children [DOM Structure] have data attributes [Attribute] that
determine how the target children will be sorted.
Surfaces Used
● sort-stuff doesn't care what its children are, only that they generate 'click'
events and the 'click' generating elements have the 'data-key' attributes.
● sort-stuff doesn't care what the elements found at data-target are, only that it
has children that have data-"key" attributes.
Only Surfaces Are Used
● Change the children of <sort-stuff>
● Change the children of sort-stuff.data-target.
Only Surfaces Are Used
<sort-stuff data-target="#sortable"> <p><input type=radio name=sort data-key=pop /> Population</p> <p><input type=radio name=sort data-key=density /> Density</p> <p><input type=radio name=sort data-key=change /> % Change</p> <sort-stuff>
<table> <tbody id=sortable> <tr data-pop=8.4 data-density=27000 data-change=3.86><td>New York</td></tr> <tr data-pop=3.9 data-density=8092 data-change=3.59><td>Los Angeles</td></tr> <tr data-pop=2.7 data-density=11842 data-change=0.99><td>Chicago</td></tr> <tr data-pop=2.2 data-density=3501 data-change=6.63><td>Houston</td></tr> <tr data-pop=1.5 data-density=11379 data-change=2.25><td>Philadelphia</td></tr> </tbody> </table>
Roll in toggle-next
Framework
Polyfills Web Components
Packaging: HTML ImportsOrganization:
EventsAttributesDOM Structure
Summary
Start writing reusable orthogonally composable units of HTML+CSS+JS
http://bit.ly/100-line-template
Data Binding
<template id=t> <div> <p><a href="{{ url }}">{{ foo.bar.baz }} {{ quux }}</a>!</p> <!-- Loop over arrays. Use data attributes of data-repeat-[name]="{{ x.y.z }}" and reference the values iterated over using {{ [name] }}. For arrays the 'i' state variable is also set. --> <ul data-repeat-num="{{ list }}"> <li>{{ num }} {{ i }}</li> </ul> <!-- Loop over Objects. For objects the 'key' state variable is also set. --> <ul data-repeat-o="{{ anobj }}"> <li>{{ key }}={{ o.name }}</li> </ul> </div> </template>
var clone = ctx.import('t');
Expand(clone, data);
document.body.appendChild(clone);
Extra Benefits
● Enabling mix and match (our use of D3.js).
● Better than native. (support and styling).○ Ever tried to style a checkbox?○ details/summary
■ http://caniuse.com/#feat=details
Missing Pieces
● CSS Variables for Shadow DOM
● Better Compilers
There are dozens of us!
● You (probably) don't need a JavaScript framework https://slack-files.com/T03JT4FC2-F151AAF7A-13fe6f98da
fin
Appendix
● http://webcomponents.org/polyfills/○ You only need webcomponents-lite.js for all the content in this slide
deck. ● http://bitworking.org/news/2014/05/zero_framework_manifesto
○ The rant that launched it all. ● http://vanilla-js.com/
But what about React Native?“learn once, write anywhere.“
https://en.wikipedia.org/wiki/List_of_platform-independent_GUI_libraries