src/base.js
'use strict';
import fireEvent from './internals/fire-event.js';
import toJsProp from './internals/to-js-prop.js';
import loadScript from './internals/load-script.js';
import PubSubLoader from './internals/pub-sub-loader.js';
import { render } from './../node_modules/lit-html/lit-html.js'
window.registeredElements = window.registeredElements || [];
const shouldShim = () => {
return /Edge/.test(navigator.userAgent) || /Firefox/.test(navigator.userAgent);
}
const isValidRenderer = renderer => {
if (!renderer) {
return;
}
return String(renderer).includes('return html`')
}
const handleProperties = (target, properties) => {
if (properties) {
for (let entry of Object.entries(properties)) {
handleProperty(target, entry[0], entry[1]);
// TODO: are we ignoring stuff ...?
// check if attribute has value else pass default property value
target[entry[0]] = target.hasAttribute(entry[0]) ? target.getAttribute(entry[0]) : entry[1].value;
}
}
}
const handleProperty = (obj, property, {observer, strict, global, reflect, render, value }) => {
if (Boolean(observer || global) && _needsObserverSetup(obj, property)) {
// Ensure we don't do duplicate work
obj.observedProperties.push(property);
// subscribe only when a callback is defined, all other global options are still available ...
if (global && obj[observer]) {
// Warning, global observers don't work the same like bindings, each observer has it's namespace created like global.name,
// so whenever another element has an global observer for name, they will subscribe to the same publisher !
// TODO: Add local binding & improve global observers
// {{name}} for normal bindings & {{global::name}} for global bindings(observers) (like Polymer does)
// this means we need to build a system that keeps track of each component it's bindings &
// values should be set as property, so we know if a value needs to be set on attribute, rerender template, etc ..
PubSub.subscribe(`global.${property}`, obj[observer].bind(obj));
}
setupObserver(obj, property, observer, {strict, global, reflect, renderer: render})
} else if (!Boolean(observer || global) && Boolean(reflect || render)) {
setupObserver(obj, property, observer, {strict, global, reflect, renderer: render})
}
}
const _needsObserverSetup = (obj, property) => {
if (!obj.observedProperties) {
obj.observedProperties = [];
}
if (obj.observedProperties[property]) {
console.warn(
'observer::ignoring duplicate property observer ' + property
);
return false;
} else {
return true;
}
}
const forObservers = (target, observers, isGlobal=false) => {
for (let observe of observers) {
let parts = observe.split(/\(|\)/g);
let fn = parts[0];
parts = parts.slice(1);
for (let property of parts) {
if (property.length) {
handleProperty(target, property, fn, {
strict: false,
global: isGlobal
});
}
}
}
}
/**
* Runs a method on target whenever given property changes
*
* example:
* change(change) {
* change.property // name of the property
* change.value // value of the property
* }
*
* @arg {object} obj target
* @arg {string} property name
* @arg {boolean} strict
* @arg {method} fn The method to run on change
*/
const setupObserver = (obj, property, fn, {strict, global, reflect, renderer}) => {
Object.defineProperty(obj, property, {
set(value) {
if (value === undefined) {
return
}
if (this[`_${property}`] === value) {
return;
}
this[`_${property}`] = value;
let data = {
property: property,
value: value
};
if (reflect) {
if (value) this.setAttribute(property, String(value));
else this.removeAttribute(property);
}
if (renderer) {
if (typeof renderer === 'boolean') {
render(this.render(), this.shadowRoot);
} else {
// adds support for multiple renderers
render(this[renderer](), this.shadowRoot);
}
}
if (global) {
data.instance = this;
PubSub.publish(`global.${property}`, data);
} else if(fn) {
if (this[fn]) {
this[fn](data);
} else {
console.warn(`observer undefined::${fn} is not a function`);
}
}
},
get() {
return this[`_${property}`];
},
configurable: strict ? false : true
});
}
const handleObservers = (target, observers=[], globalObservers=[]) => {
if (!observers && !globalObservers) {
return;
}
forObservers(target, observers);
}
const handleListeners = target => {
const attributes = target.attributes
for (const attribute of attributes) {
if (String(attribute.name).includes('on-')) {
const fn = attribute.value;
const name = attribute.name.replace('on-', '');
target.addEventListener(String(name), event => {
target = event.path[0];
while (!target.host) {
target = target.parentNode;
}
if (target.host[fn]) {
target.host[fn](event);
}
});
}
}
}
const ready = target => {
requestAnimationFrame(() => {
if (target.ready) target.ready();
});
}
const constructorCallback = (target=HTMLElement, klass=Function, hasWindow=false) => {
PubSubLoader(hasWindow);
target.fireEvent = target.fireEvent || fireEvent.bind(target);
target.toJsProp = target.toJsProp || toJsProp.bind(target);
target.loadScript = target.loadScript || loadScript.bind(target);
// setup properties
handleProperties(target, klass.properties);
handleObservers(target, klass.observers, klass.globalObservers);
if (!target.registered && target.created) target.created();
// let backed know the element is registered
target.registered = true;
}
const connectedCallback = (target=HTMLElement, klass=Function) => {
if (target.connected) target.connected();
// setup listeners
handleListeners(target)
// notify everything is ready
ready(target);
}
const shouldRegister = (name, klass) => {
if (window.registeredElements.indexOf(name) === -1) {
window.registeredElements.push(name);
return true;
}
return false;
}
export default {
ready: ready,
connectedCallback: connectedCallback,
constructorCallback: constructorCallback,
shouldRegister: shouldRegister
}