Like before we can make a simple Hello World example using the FancyPants system.
<select>
<option value="">-- Pick One --</option>
<option value="Jane">Jane</option>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
<option value="Charlie">Charlie</option>
</select>
<hello-world></hello-world>
import Component from '../component.js';
class HelloWorld extends Component {
render() {
this.innerHTML = `
<style>
span { background-color: #bada55; }
</style>
<span>Hello ${this.name}</span>
`;
}
get name() {
return this.getAttribute('name') || 'World';
}
static get observedAttributes() {
return ['name'];
}
}
HelloWorld.register();
document.querySelector('select').addEventListener('change', e => {
document.querySelector('hello-world').setAttribute('name', e.target.value);
});
In this example the re-rendering or update dynamic content is driven by the
setAttribute()
which is tracked and when it changes it will ask the rendering
engine to run the component's render()
function. In this case assigning
a new innerHTML
.
With FancyPants any attributes defined in observedAttributes
will be auto
tracked.
Tracking
The power of FancyPants is in how it manages dirty tracking. How this works is highly technical and best described by Chris Garrett's blog post "How Autotracking Works" (2020-02-26).
For us it means that if a property is marked as tracked when it changes it
will initiate a render loop where all Component's will run their render()
functions only if those render functions depend on the thing that just
changed.
A simple example say we have an object every Component uses.
const theFruit = activateTracking({
name: tracked(),
count: tracked()
});
And a component that uses it.
class MyComponent extends Component {
render() {
this.innerHTML = `${theFruit.name}: ${theFruit.count}`;
}
}
Then the component's innerHTML would update when theFruit
properties change.
theFruit.name = 'Apples'; // schedules a render loop
theFruit.count = 4; // schedules a render loop
This done by making the name
and count
properties getters/setters. That way
we can track when they are either assigned (set) or consumed (get). Whith this
information the system can determine which render()
functions depend on which
property and if they should run or not run depending on if one of its
dependents changed or not.
This even works if the dependency is indirect like this.
function makeFruitString() {
return `${theFruit.name}: ${theFruit.count}`;
}
class MyComponent extends Component {
render() {
this.innerHTML = makeFruitString();
}
}
In fact the system affords the ability to be even more granular if needed. In this example we mark separate methods to be autotracked and only the one which need to run will run.
class MyComponent extends Component {
constructor() {
super();
this.doExpensiveDOMUpdateWithName = memoizeFunction(this.doExpensiveDOMUpdateWithName);
this.doExpensiveDOMUpdateWithCount = memoizeFunction(this.doExpensiveDOMUpdateWithCount);
}
render() {
this.doExpensiveDOMUpdateWithName();
this.doExpensiveDOMUpdateWithCount();
}
doExpensiveDOMUpdateWithName() { … }
doExpensiveDOMUpdateWithCount() { … }
}
In this case when theFruit.name = 'Orange'
happens the render()
is called
which will run both methods. But since only name
changed only
doExpensiveDOMUpdateWithName()
is executed. doExpensiveDOMUpdateWithCount()
is just a no-op unless count
was also updated.