So you want to make a primary results component
var ElementBase = require("../elementBase.js"); | |
var Retriever = require("../retriever.js"); | |
/* | |
Let's make a web component the elections20-primaries way! We're going to start by | |
subclassing our base element, since it makes the setup a little more declarative. | |
You'll see how that works in a minute. | |
*/ | |
class DemoElement extends ElementBase { | |
/* | |
The constructor is called whenever we (or the browser) creates one of these | |
elements. We can use this to set up local state for this particular element, in | |
case it needs to store any values. | |
Crucially, you _cannot_ do anything in the constructor that changes the inside | |
of the tag itself (such as appending children or using innerHTML). We'll save | |
that for later. | |
*/ | |
constructor() { | |
// the constructor _must_ call super() first, which is basically saying "run the setup for the parent class (ElementBase)" | |
super(); | |
// let's add a retriever that watches a data file | |
this.fetch = new Retriever(this.load); | |
} | |
/* | |
In the constructor, we passed this.load directly to the retriever. We want | |
that function to always have this element as `this`, even if it's called | |
from an event listener like that. By adding it to the static boundMethods() | |
getter, ElementBase will bind it permanently for us. We'll also bind some | |
event handlers, to simplify later code. | |
*/ | |
static get boundMethods() { | |
return ["load", "onClick"] | |
} | |
/* | |
Similarly to the way we bound methods, we can also ask the browser to let us | |
know when attributes are added, changed, or removed, by specifying the static | |
observedAttributes() getter. Unlike boundMethods, which comes from our custom | |
base class, this is actually a part of the built-in custom elements API. | |
*/ | |
static get observedAttributes() { | |
return ["src"]; | |
} | |
/* | |
We probably want attributes to be mirrored as properties--it should be possible | |
to set the source two ways: | |
- element.setAttribute("src", "x"); | |
- element.src = x; | |
Any element that inherits from ElementBase can have this behavior for free, | |
just by creating a static mirroredProps() getter: | |
*/ | |
static get mirroredProps() { | |
return ["src"]; | |
} | |
/* | |
And here's where we get alerted to attribute changes. This callback gets | |
three arguments: the name of the attribute, the old value, and the new | |
value. If the attribute was added, the old value will be null, and if it was | |
removed, the new value is null. Otherwise, it'll be a string (but possibly | |
an empty string, like ""). | |
attributeChangedCallback() is not just called once the element is on the | |
page. It's also called during startup for any elements that the browser finds | |
while reading the document. If we have <demo-element src="x"></demo-element> | |
in our page, this will be called with `"src", null, "x"` when the element is | |
first detected, after the constructor runs. | |
*/ | |
attributeChangedCallback(attr, oldValue, currentValue) { | |
// a switch is a nice way to handle this function, since it lets us block out changes and offer default behavior | |
switch (attr) { | |
case "src": | |
// when the src is set, start watching the file. | |
this.fetch.watch(value); | |
break; | |
} | |
} | |
/* | |
One last little bit of set-up. Again, we're not allowed to change the contents | |
of the element in the constructor. Our base element does try to smooth this | |
out, however, by providing a standard way to define its inner markup in a | |
two-step process. The first step is to provide a static template() getter, | |
which returns an HTML string (either inline, as here, or required from another | |
file). | |
*/ | |
static get template() { | |
return ` | |
<pre data-as="output"></pre> | |
<button data-as="clicker">Click me</button> | |
`; | |
} | |
/* | |
At this point, our element has probably gone through the following steps: | |
1. It was created, either in markup or via document.createElement("demo-element"); | |
2. The browser called our constructor, which set up a Retriever to watch files | |
3. The browser saw our src attribute and called attributeChangedCallback so we start monitoring its URL | |
4. Having a URL, the retriever has performed a fetch() to get the JSON file | |
5. With that file loaded, it calls our load() method and passes in the data. | |
6. If the retriever detects changes to the file, it'll call load() again. | |
*/ | |
load(data) { | |
// Now we'll fill our template into the element, using the illuminate() method (which is defined on the ElementBase) | |
var elements = this.illuminate(); | |
// elements will contain a property for anything tagged with a data-as attribute. | |
// let's print out the JSON data we got from the retriever in our <pre> tag: | |
elements.output.innerHTML = JSON.stringify(data, null, 2); | |
} | |
/* | |
ElementBase.illuminate() is probably the oddest part of our setup. Used in the | |
simplest way, it's just for splatting a template into an element and saving you | |
a call to this.querySelector(). But you can also use it to safely run setup | |
code that depends on that template markup directly. | |
Why is it called illuminate()? Well, there are two kinds of DOM that are | |
available to custom elements. There's the light DOM, which is the standard HTML | |
content you're used to. And then there's the shadow DOM, which is "hidden" from | |
scripts on the page. For example, the playback controls in a <video> tag are | |
built using shadow DOM. Shadow DOM is not just isolated from JavaScript, it's | |
also unaffected by page styles, which is useful when you're building a UI | |
component and you don't want the page's fonts or colors to mess with your code. | |
And while you're not allowed to touch the contents of an element in its | |
constructor, you _can_ mess with its Shadow DOM. | |
We're not using shadow DOM because we don't need that isolation, and in fact it | |
would just make things more complicated. But that means we need a way of filling | |
the light DOM. Hence, illuminate(). | |
illuminate() does two things: first, it will fill the element with the contents | |
of your template() getter, but only the first time it's called (so it's safe to | |
call it as many times as you want). Second, it returns an object containing all | |
the elements you tagged with a data-as attribute, so you can access them. | |
We can take advantage of the write-once behavior of illuminate() by overriding | |
the base class's method with our own, and using it to add event listeners to | |
the templated HTML elements. | |
*/ | |
illuminate() { | |
// first, we still need to call the ElementBase.illuminate() method to inject our template and get elements back | |
var elements = super.illuminate(); | |
// next, we can add listeners, like our bound onClick() method | |
// this code to add the listener will only run once, no matter how many times you call this.illuminate() | |
elements.clicker.addEventListener("click", this.onClick); | |
// finally, we still need to return our elements, so that methods like load() can use them | |
return elements; | |
} | |
onClick() { | |
// remember, this is bound, so `this` will always be the element itself | |
console.log("You clicked the button in ", this); | |
} | |
} | |
// Now we define the element as a tag | |
// The class method wraps this in a try-catch for us, which is nice. | |
DemoElement.define("demo-element"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment