Skip to content

Instantly share code, notes, and snippets.

@evilbuck
Created April 14, 2023 16:33
Show Gist options
  • Select an option

  • Save evilbuck/4d3ddcf1a04e10511e4c5f840f07800f to your computer and use it in GitHub Desktop.

Select an option

Save evilbuck/4d3ddcf1a04e10511e4c5f840f07800f to your computer and use it in GitHub Desktop.
Web Component
import _ from 'lodash';
export default class BaseWebComponent extends HTMLElement {
store = null;
events = [];
_previouslyRendered = false;
constructor({ controller, model, store, template }) {
super();
this.store = store;
this.controller = controller;
this.model = model;
// TODO: enable this to allow passing in a string or a dom element
// if (typeof template === string) {
// this.template = document.querySelector(template);
// }
this.template = template;
const instance = this.template.content.cloneNode(true);
this.instance = instance;
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(instance);
}
// called when the component is attached to the dom
connectedCallback() {
this._connectEvents();
}
// called when the component is removed from the dom
disconnectedCallback() {
this._cleanupEvents();
}
_connectEvents() {
// get all the elements with a data-oh_onclick attribute
const onClickEls = this.shadowRoot.querySelectorAll('[data-oh_onclick]');
onClickEls.forEach((el) => {
const [directive, argsString] = el.dataset.oh_onclick.split(':').map((s) => s.trim());
const args = argsString ? argsString.split(',').map((s) => s.trim()) : [];
const func = this.methods[directive].bind(this, ...args);
// we're saving the event listener so we can remove it during cleanup
this.events.push({ el, func, directive, type: 'click' });
el.addEventListener('click', func);
});
}
_cleanupEvents() {
this.events.forEach(({ el, func, type }) => {
el.removeEventListener(type, func);
});
// clear the events since we removed them
this.events = [];
}
// manually called, generally by setting up a reaction to the store
// sole purpose is to update the shadowRoot
render() {
this._cleanupEvents();
// get all the elements with a data-oh_bind attribute for binding the textNode to the dom element
this.shadowRoot.querySelectorAll('[data-oh_bind]').forEach((element) => {
// parse the data-oh_bind attribute to get the property name
const dataBind = element.dataset.oh_bind;
const [directive, property] = dataBind.split(':').map((s) => s.trim());
switch (directive) {
case 'text':
element.textContent = _.get(this, property) ?? _.get(this.store, property, '');
console.log('text', element.textContent, this.model, property);
break;
default:
if (process.env.NODE_ENV === 'development') {
console.warn(`Unknown directive ${directive} in data-oh_bind`);
}
break;
}
});
this._connectEvents();
}
_get(object, path, defaultValue) {
return _.get(object, path, defaultValue);
}
}
// Usage
class Button extends BaseWebComponent {
buttonText = 'change me';
constructor(...args) {
super(...args);
makeObservable(this, {
buttonText: observable
});
// watch for changes to buttonText
reaction(() => this.buttonText, (value, oldValue) => {
if (oldValue !== value) {
this.render();
}
});
}
methods: {
handleClick: () => {
console.log('clicked');
runInAction(() => {
this.buttonText = "I changed";
});
}
}
}
// html
<!DOCTYPE>
<html>
<body>
<template id="button-template">
<button data-oh_bind="text: buttonText" data-oh_onclick="handleClick">change me</button>
</template>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment