fwebc.js

Toy framework for learning web components
git clone git://git.finwo.net/lib/fwebc.js
Log | Files | Refs | README

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 });