DEV Community

artydev
artydev

Posted on • Edited on

Create WebComponents in declarative way

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

Top comments (0)