UNPKG

@anywhichway/lazui

Version:

Single page apps and lazy loading sites with minimal JavaScript or client build processes.

210 lines (201 loc) 10.1 kB
const imports = { } const isPrimitive = (value) => ["bigint","boolean","number","string","symbol"].includes(typeof value); const formToJSON = (el,JSON,includeEmpty) => { return [...el.elements].reduce((json,element) => { if(["submit","button","image","reset"].includes(element.type)) return json; if(element.type==="checkbox") { json[element.name] = element.checked; return json; } let value = element.value; if(value!=="" || includeEmpty) { if(value==="") value = typeof includeEmpty === "string" ? includeEmpty : ""; try { value = JSON.parse(value); } catch { } if(element.type==="radio") { if(element.checked) json[element.name] = value; } else { json[element.name] =value; } } return json; },{}) } const getInputElementHTML = (name,value,{bind,prefix,path}) => { const type = typeof value, property = [...path,name].join("."); if(type==="boolean") { return `<label for="${name}">${name}:</label><input type="checkbox" name="${name}" ${value ? "checked" : ""} ${bind ? `${prefix}:bind="${property}"` : ""}>` } if(type==="number" || type==="bigint") { return `<input type="number" name="${name}" title="${name}" placeholder="${name}" ${bind ? `${prefix}:bind="${property}"` : ""}>` } if(type==="string") { if(value.length<=50) { return `<input type="text" name="${name}" title="${name}" placeholder="${name}" ${bind ? `${prefix}:bind="${property}"` : ""}>` } return `<textarea name="${name}" title="${name}" placeholder="${name}" ${bind ? `${prefix}:bind="${property}"` : ""}>${value}</textarea>` } if(Array.isArray(value) && value.every((item) => isPrimitive(item))) { return `<input type="text" name="${name}" title="${name}" placeholder="${name}" value="${value.join(", ")}" ${bind ? `${prefix}:bind="${property}"` : ""}>` } } const getValue = (input,property,context) => { let value; if(input.type==="select-multiple") { value = [...input.selectedOptions].map((option) => { try { return JSON.parse(option.value) } catch { return option.value; } }); } else if(input.type==="checkbox") { value = input.checked; } else { try { value = JSON.parse(input.value); } catch { value = input.value; } } if(context[property]==null || (Array.isArray(context[property]) && context[property].every((item) => isPrimitive(item)))) { value = value.split(",").map((value) => { value = value.trim() try { return JSON.parse(value); } catch { return value; } }); if(!value.every((item) => isPrimitive(item))) { return; } } return value; } const addInputs = (table,object,options,prefix,path=[]) => { Object.entries(object).forEach(([key,value]) => { if(value && typeof value==="object") return addInputs(table,value,options,prefix,[...path,key]); const html = getInputElementHTML(key,value,{bind:true,prefix,path}); if(html) { const row = document.createElement("tr"), cell = document.createElement("td"); if(options.useLabels) { const label = document.createElement("label"); label.setAttribute("for",key); label.innerText = key[0].toUpperCase() + key.slice(1) + ":"; row.appendChild(label); } cell.innerHTML = html; row.appendChild(cell); table.appendChild(row); } }) } const init = async ({el,root,state,lazui,options})=> { if(el.tagName!=="FORM") throw new TypeError("lz:form: el must be a form element"); const {getContext,JSON,router,render,interpolate,prefix,handleDirective} = lazui; if(el.innerHTML.trim()==="") { if(!el.__state__ && el.hasAttribute(`${prefix}:usestate`)) { await handleDirective(el.attributes[`${prefix}:usestate`],{state,root}) } //const context = getContext(el); // does not work, can't iterate over keys for some reason const context = el.__state__; if(el.hasAttribute(`${prefix}:src`)) { el.innerHTML = await fetch(el.getAttribute("data-lz:src")).then((response) => response.text()); } else { const table = document.createElement("table"); addInputs(table,context,options,prefix); el.appendChild(table); if(el.hasAttribute("action")) { el.insertAdjacentHTML("beforeend",`<br><button type="submit">Submit</button>`); } } await init({el,root,state:context,lazui,options}); return; } for(const input of el.querySelectorAll("input,select,textarea")) { let property = input.getAttribute("data-lz:bind:read") || input.getAttribute("data-lz:bind:write") || input.getAttribute("data-lz:bind"); if(!property && (input.hasAttribute("data-lz:bind:read") || input.hasAttribute("data-lz:bind:write") || input.hasAttribute("data-lz:bind"))) { property = input.getAttribute("name"); } if(property) { if(!input.hasAttribute("name")) input.setAttribute("name",property); if(input.hasAttribute("data-lz:bind:write") || input.hasAttribute("data-lz:bind")) { const listener = (event) => { const context = getContext(el), value = getValue(input,property,context); if(value!=null) context.set(property,value); } if(options.bind!=="submit") input.addEventListener(options.bind||"input",listener); } if(input.hasAttribute("data-lz:bind:read") || input.hasAttribute("data-lz:bind")) { const context = getContext(el), value = context.get(property); if(input.type==="checkbox") { input.checked = !!value; } else if(input.type==="select-multiple") { for(const option of input.options) { if(value.includes(option.value)) option.selected = true; } } else if(input.type==="radio") { if(input.value===value) input.checked = true; } else if(value!=null) { input.value = isPrimitive(value) ? value : JSON.stringify(value); } } } if(!input.hasAttribute("placeholder")) input.setAttribute("placeholder",input.getAttribute("name")); if(!input.hasAttribute("title")) input.setAttribute("title",input.getAttribute("name")); } el.addEventListener("submit",async(event) => { event.preventDefault(); if(options.bind==="submit") { const context = getContext(el); for(const input of el.querySelectorAll("input,select,textarea")) { if(input.hasAttribute(`${prefix}:bind:write`) || input.hasAttribute(`${prefix}:bind`)) { const property = input.getAttribute(`${prefix}:bind:write`) || input.getAttribute(`${prefix}:bind`) || input.getAttribute("name"), value = getValue(input,property,context); if(value!=null) context.set(property,value); } } } const action = el.getAttribute("action"); if(!action) throw new Error("Form must have an action attribute in order to submit"); let config; const enctype = el.getAttribute("enctype")||"application/x-www-form-urlencoded", headers = {"Content-Type":enctype}; let format; if(enctype==="application/json") format = () => JSON.stringify(formToJSON(el,JSON)); else if(enctype==="application/x-www-form-urlencoded") format = () => new URLSearchParams(new FormData(el)).toString(); else if(enctype==="multipart/form-data") format = () => new FormData(el); else if(enctype==="text/plain")format = () => el.innerText; else throw new TypeError(`Form options.format must be one of "application/x-www-form-urlencoded", "application/json", "multipart/form-data", "text/plain" not ${enctype}.`); const response = await router.fetch(new Request(action,{method:el.getAttribute("method")||"POST",body:format(),headers})); let content; if(response.ok) { content = await response.text(); if(["html","template"].includes(options.expect)) content = new DOMParser().parseFromString(content,"text/html"); } else { content = `${response.status} ${response.statusText}` } const where = el.hasAttribute("data-lz:target") ? el.getAttribute("data-lz:target") : undefined; if(options.template) { if(options.expect && options.expect!=="json") throw new TypeError(`A form has specified an output template ${options.template} but expects a response of type ${options.expect}. It should expect JSON instead.`); const template = document.querySelector(options.template); render(el,interpolate(template.innerHTML,getContext(el,Object.assign(formToJSON(el,JSON,"unknown"),JSON.parse(content)))),{root,where}); } else if(options.expect==="template") { render(el,content,{root,where,state:getContext(el,formToJSON(el,JSON,"unknown"))}); } else { // expect===html render(el,content,{root,where}); } }) } export { imports, init }