fwebc.js (4655B)
1 ;(factory => { 2 if (('object' === typeof module) && ('exports' in module)) { 3 module.exports = factory({ 4 fetch: require('node-fetch'), 5 }); 6 } else if ('object' === typeof window) { 7 window.fwebc = factory({ 8 fetch: window.fetch, 9 }); 10 } 11 })(({fetch}) => { 12 const fwebc = {}; 13 const plugins = []; 14 const config = { 15 ext: 'tag', 16 base: '/partial', 17 }; 18 19 const util = fwebc.util = { 20 unescape(input) { 21 const el = document.createElement('textarea'); 22 el.innerHTML = input; 23 return el.childNodes.length === 0 ? "" : el.childNodes[0].nodeValue; 24 }, 25 observable(obj, callback, prefix = '') { 26 if (Object(obj) !== obj) throw new Error(`Object is not an object, got: ${obj}`); 27 if ('function' !== typeof callback) throw new Error(`Callback is not a function, got: ${callback}`); 28 for(const key of Object.keys(obj)) { 29 if (Object(obj[key]) !== obj[key]) continue; 30 obj[key] = util.observable(obj[key], callback, `${prefix}${key}.`); 31 } 32 return new Proxy(obj, { 33 set(target, name, value, receiver) { 34 var oldVal = target[name]; 35 if (oldVal === value) return; 36 let type = name in target ? 'update' : 'add'; 37 const record = { name, type, object: target }; 38 if (type == 'update') record.oldValue = target[name]; 39 target[name] = Object(value) === value ? util.observable(value,callback,`${prefix}${name}.`) : value; 40 callback([record]); 41 return true; 42 }, 43 deleteProperty(target, name, value) { 44 if (!(name in target)) return; 45 const record = { name, type: 'delete', object: target, oldValue: target[name] }; 46 delete target[name]; 47 callback([record]); 48 return true; 49 }, 50 }); 51 }, 52 }; 53 54 // Override configs 55 fwebc.cfg = cfg => { 56 Object.assign(config, cfg); 57 }; 58 59 // Install a plugin 60 fwebc.install = callback => { 61 if ('function' !== typeof callback) return; 62 plugins.push(callback); 63 }; 64 65 // Remove a plugin 66 fwebc.uninstall = callback => { 67 const idx = plugins.indexOf(callback); 68 if (!~idx) return; 69 plugins.splice(idx, 1); 70 }; 71 72 // Register a component 73 fwebc.register = (name, source) => { 74 if (window.customElements.get(name)) return; 75 76 // Parse remplate 77 const wrapper = document.createElement('template'); 78 wrapper.innerHTML = source; 79 80 // Separate style, script & template 81 let template = null; 82 let scripts = []; 83 let styles = []; 84 for(const node of [...wrapper.content.children]) { 85 if (node instanceof HTMLTemplateElement) template = node; 86 if (node instanceof HTMLScriptElement ) {scripts.push(node);wrapper.content.removeChild(node);} 87 if (node instanceof HTMLStyleElement ) {styles.push(node);wrapper.content.removeChild(node);} 88 } 89 if (!template) { 90 template = wrapper; 91 } 92 93 // Convert template and code into a string 94 template = util.unescape(template.innerHTML); 95 let code = ''; 96 for(const script of scripts) { 97 if (script.getAttribute('src')) continue; 98 code += script.innerHTML; 99 } 100 101 // Register the actual element 102 window.customElements.define(name, class extends HTMLElement { 103 constructor() { 104 super(); 105 this.root = this.attachShadow({ mode: 'open' }); 106 this.state = {}; 107 for(const plugin of plugins) plugin(this); 108 (new Function(code)).call(this); 109 if (this.dependencies) this.dependencies.forEach(fwebc.load); 110 this.state = util.observable(this.state, () => this.emit('update')); 111 this.on('update', this.render.bind(this)); 112 this.render(); 113 } 114 render() { 115 const fn = new Function(...Object.keys(this.state), 'return `'+template+'`;'); 116 const stylez = Array 117 .from(this.root.ownerDocument.styleSheets) 118 .map(stylesheet => stylesheet.ownerNode.outerHTML) 119 .concat(styles.map(stylesheet => stylesheet.outerHTML)); 120 try { 121 this.root.innerHTML = stylez.join('') + fn.call(this, ...Object.values(this.state)); 122 } catch(e) { 123 console.error(e); 124 } 125 } 126 emit(event, data = {}) { 127 const ev = new CustomEvent(event, data); 128 this.dispatchEvent(ev); 129 } 130 on(event, handler) { 131 this.addEventListener(event, handler); 132 } 133 }); 134 }; 135 136 // Load a component 137 fwebc.load = name => { 138 fetch(`${config.base}/${name.replace(/-/g,'/')}.${config.ext}`) 139 .then(res => res.text()) 140 .then(source => { 141 fwebc.register(name, source); 142 }); 143 }; 144 145 return fwebc; 146 });