Create WebComponents in declarative way
artydev

artydev @artydev

Joined:
Jul 17, 2019

Create WebComponents in declarative way

Publish Date: Mar 10
1 0

Many backend developpers are reluctant at using Javascript.
Needless to say, don't try to convince them to create WebComponents in Javascript.

So here is a nice solution : facet.

It allows to create WebComponents declaratively using 'template' tags

Here is a demo :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        // Facet v0.1.2a | https://github.com/kgscialdone/facet
const facet=new function(){this.version="0.1.2a";this.defineComponent=function defineComponent(tagName,template,{shadowMode:shadowMode="closed",observeAttrs:observeAttrs=[],applyMixins:applyMixins=[],localFilters:localFilters={},extendsElement:extendsElement,formAssoc:formAssoc=false}){const extendsConstr=extendsElement?document.createElement(extendsElement).constructor:HTMLElement;const extendsOptions=extendsElement?{extends:extendsElement}:undefined;window.customElements.define(tagName,class FacetComponent extends extendsConstr{static observedAttributes=observeAttrs;static formAssociated=formAssoc;#root=shadowMode!=="none"?this.attachShadow({mode:shadowMode}):this;#localFilters={...localFilters};constructor(){super();if(formAssoc){let internals=this.attachInternals(),value;Object.defineProperties(this,{internals:{value:internals,writable:false},value:{get:()=>value,set:newValue=>internals.setFormValue(value=newValue)},name:{get:()=>this.getAttribute("name")},form:{get:()=>internals.form},labels:{get:()=>internals.labels},validity:{get:()=>internals.validity},validationMessage:{get:()=>internals.validationMessage},willValidate:{get:()=>internals.willValidate},setFormValue:{value:(n,s)=>internals.setFormValue(value=n,s),writable:false},setValidity:{value:internals.setValidity.bind(internals),writable:false},checkValidity:{value:internals.checkValidity.bind(internals),writable:false},reportValidity:{value:internals.reportValidity.bind(internals),writable:false}})}}connectedCallback(){const content=template.content.cloneNode(true);const mixins=Object.values(facet.mixins).filter((m=>m.applyGlobally||applyMixins.includes(m.name)));for(let mixin of mixins){content[mixin.attachPosition](mixin.template.content.cloneNode(true));Object.assign(this.#localFilters,mixin.localFilters)}for(let script of content.querySelectorAll("script[on]")){let parent=script.parentElement??this;let handler=new Function("host","root","event",script.innerText).bind(parent,this,this.#root);for(let event of script.getAttribute("on").split(/\s+/g))parent.addEventListener(event,handler,{once:script.hasAttribute("once"),capture:script.hasAttribute("capture"),...script.hasAttribute("passive")?{passive:true}:{}});script.remove()}for(let el of content.querySelectorAll("[inherit]")){for(let attr of el.getAttribute("inherit").split(/\s+/g)){const[,ogname,rename,fn]=attr.match(/^([^\/>"'=]+)(?:>([^\/>"'=]+))?(?:\/(\w+))?$/);const cv=this.getAttribute(ogname),filter=this.#localFilters[fn]?.bind(this,this,this.#root)??window[fn];if(cv)el.setAttribute(rename??ogname,filter?.(cv,undefined,el,this)??cv);if(observeAttrs.includes(ogname))this.addEventListener("attributeChanged",(({detail:{name:name,oldValue:oldValue,newValue:newValue}})=>{if(name!==ogname)return;el.setAttribute(rename??ogname,filter?.(newValue,oldValue,el,this)??newValue)}))}el.removeAttribute("inherit")}if(formAssoc)this.value=this.getAttribute("value");this.#root.append(content);this.#event("connect")}disconnectedCallback(){this.#event("disconnect")}adoptedCallback(){this.#event("adopt")}attributeChangedCallback(name,oldValue,newValue){this.#event("attributeChanged",{name:name,oldValue:oldValue,newValue:newValue})}formAssociatedCallback(form){this.#event("formAssociate",{form:form})}formDisabledCallback(disabled){this.#event("formDisable",{disabled:disabled})}formResetCallback(){this.#event("formReset")}formStateRestoreCallback(state,mode){this.#event("formStateRestore",{state:state,mode:mode})}#event(n,d={}){this.dispatchEvent(new CustomEvent(n,{detail:{...d,component:this}}))}},extendsOptions)};this.defineMixin=function defineMixin(name,template,options){this.mixins[name]={...options,name:name,template:template}};this.discoverDeclarativeComponents=function discoverDeclarativeComponents(root){let mixinSelector=`template[${facet.config.namespace}mixin]:not([defined])`;let cmpntSelector=`template[${facet.config.namespace}component]:not([defined])`;if(root.matches?.(mixinSelector))processMixin(root);if(root.matches?.(cmpntSelector))processComponent(root);for(let template of root.querySelectorAll(mixinSelector))processMixin(template);for(let template of root.querySelectorAll(cmpntSelector))processComponent(template);function processMixin(template){template.setAttribute("defined",true);facet.defineMixin(template.getAttribute(`${facet.config.namespace}mixin`),template,{applyGlobally:template.hasAttribute("global"),attachPosition:template.hasAttribute("prepend")?"prepend":"append",localFilters:discoverLocalFilters(template)})}function processComponent(template){template.setAttribute("defined",true);facet.defineComponent(template.getAttribute(`${facet.config.namespace}component`),template,{shadowMode:template.getAttribute("shadow")?.toLowerCase()??facet.config.defaultShadowMode,observeAttrs:template.getAttribute("observe")?.split(/\s+/g)??[],applyMixins:template.getAttribute("mixins")?.split(/\s+/g)??[],localFilters:discoverLocalFilters(template),extendsElement:template.getAttribute("extends"),formAssoc:template.hasAttribute("forminput")})}function discoverLocalFilters(template){return[...template.content.querySelectorAll("script[filter]")].map((script=>{script.remove();return[script.getAttribute("filter"),new Function("host","root","value",script.innerText)]})).reduce(((a,[k,v])=>{a[k]=v;return a}),{})}};this.mixins={};this.config={namespace:document.currentScript?.hasAttribute?.("namespace")?document.currentScript.getAttribute("namespace")||"facet-":"",autoDiscover:document.currentScript&&!document.currentScript.hasAttribute("libonly"),defaultShadowMode:document.currentScript?.getAttribute("shadow")??"closed"};(fn=>document.readyState==="interactive"?fn():document.addEventListener("DOMContentLoaded",fn,{once:true}))((()=>this.config.autoDiscover&&this.discoverDeclarativeComponents(document)))};
    </script>
</head>
<body>

<template component="hello-world">
    <h2>Hello, <slot>world</slot>!</h2>
</template>

<hello-world></hello-world>
<hello-world>Facet</hello-world> 


<inc-dec  style="display: block;"   value="0"></inc-dec>

<template component="inc-dec" forminput>
  <script on="connect" once>host.innerText = host.value</script>
   <p>I am a simple counter</p>
  <button>+ <script on="click">host.innerText = ++host.value</script></button>
  <span><slot></slot></span>
  <button>- <script on="click">host.innerText = --host.value</script></button>
</template>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Image description

Comments 0 total

    Add comment