Created
July 24, 2018 03:04
-
-
Save tim-evans/e1ac78674f94d0474b57a108b775701e to your computer and use it in GitHub Desktop.
Web Component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface EventCallback { | |
(evt: Event): boolean; | |
} | |
export interface EventHandlerDefinitions { | |
[key: string]: string | EventCallback; | |
} | |
export interface EventHandlerReferences { | |
[key: string]: EventCallback; | |
} | |
function getEventNameAndElement(element: HTMLElement, definition: string) { | |
let [eventName, ...selectors] = definition.split(' '); | |
let selector = selectors.join(' '); | |
if (selector === 'document') { | |
return { eventName, element: document }; | |
} else if (selector === 'window') { | |
return { eventName, element: window }; | |
} else if (selector === '') { | |
return { eventName, element }; | |
} else { | |
let querySelector; | |
if (element.shadowRoot) { | |
querySelector = element.shadowRoot.querySelector(selector) || element.querySelector(selector); | |
} else { | |
querySelector = element.querySelector(selector); | |
} | |
return { eventName, element: querySelector }; | |
} | |
} | |
interface TemplateElement extends HTMLElement { | |
content: Node; | |
} | |
export function define(name: string, component: typeof HTMLElement) { | |
if (!window.customElements.get(name)) { | |
window.customElements.define(name, component) | |
} | |
return component; | |
}; | |
/** | |
* The events mixin is a Backbone-flavored event management system | |
* that automatically sets up and tears down events on web components | |
* when they are connected and disconnected from a document. | |
* | |
* To use this, include the events mixin on your web component, | |
* and add a static property called `events` that provides a lookup | |
* table to the events that you'd like to attach to your component. | |
* | |
* ```js | |
* import events from './mixins/events'; | |
* | |
* export default TextSelection extends events(HTMLElement) { | |
* static events = { | |
* 'selectionchange document': 'selectedTextDidChange', | |
* 'mousedown': 'willSelectText', | |
* 'mouseup': 'didSelectText' | |
* }; | |
* } | |
* ``` | |
* | |
* The selectors for `window` and `document` will select only those | |
* elements; all other selectors will lookup in the scope of the web | |
* component. This allows components to look at events like scrolling, | |
* resizing, and selection events without using `addEventListener` / | |
* `removeEventListener`. | |
*/ | |
export default class Component extends HTMLElement { | |
static events: EventHandlerDefinitions | null; | |
static template: string; | |
static style: string | null; | |
private static compiledElement: TemplateElement; | |
private static get compiledTemplate(): TemplateElement { | |
if (!this.compiledElement) { | |
this.compiledElement = document.createElement('template'); | |
let scopedStyles = this.style; | |
let html = this.template; | |
if (scopedStyles) { | |
html = `<style>${scopedStyles}</style>${html}`; | |
} | |
this.compiledElement.innerHTML = html; | |
} | |
return this.compiledElement; | |
} | |
private eventHandlers: EventHandlerReferences; | |
constructor() { | |
super(); | |
this.eventHandlers = {}; | |
let ComponentClass = this.constructor as typeof Component; | |
let shadowRoot = this.attachShadow({ mode: 'open' }); | |
shadowRoot.appendChild(ComponentClass.compiledTemplate.content.cloneNode(true)); | |
} | |
connectedCallback() { | |
let ComponentClass = this.constructor as typeof Component; | |
let events: EventHandlerDefinitions = ComponentClass.events || {}; | |
Object.keys(events).forEach((definition: string) => { | |
let { eventName, element } = getEventNameAndElement(this, definition); | |
let method = events[definition]; | |
this.eventHandlers[definition] = (evt): boolean | never => { | |
if (typeof method === 'string') { | |
if (this[method]) { | |
return this[method](evt); | |
} else { | |
throw new Error(`😭 \`${method}\` was not defined on ${this.tagName}- did you misspell or forget to add it?`); | |
} | |
} else { | |
return method.call(this, evt); | |
} | |
}; | |
element.addEventListener(eventName, this.eventHandlers[definition]); | |
}); | |
} | |
disconnectedCallback() { | |
Object.keys(this.eventHandlers).forEach((definition) => { | |
let { eventName, element } = getEventNameAndElement(this, definition); | |
element.removeEventListener(eventName, this.eventHandlers[definition]); | |
}); | |
this.eventHandlers = {}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Component, { define } from '../src/component'; | |
import template from './template.html'; | |
export default define('my-first-component', class MyFirstComponent extends Component { | |
static template = template; | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Hello, <slot></slot>! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment