@saltcorn/markup
Advanced tools
+70
| const { a, input, div, ul, text, text_attr } = require("./tags"); | ||
| const { renderForm } = require("."); | ||
| class Form { | ||
| constructor(o) { | ||
| Object.entries(o).forEach(([k, v]) => { | ||
| this[k] = v; | ||
| }); | ||
| } | ||
| } | ||
| const nolines = s => s.split("\n").join(""); | ||
| describe("form render", () => { | ||
| it("renders a simple form", () => { | ||
| const form = new Form({ | ||
| action: "/", | ||
| fields: [ | ||
| { | ||
| name: "name", | ||
| label: "Name", | ||
| input_type: "text" | ||
| } | ||
| ] | ||
| }); | ||
| const want = `<form action="/" class="form-namespace undefined" method="post" > | ||
| <input type="hidden" name="_csrf" value=""><div class="form-group"> | ||
| <label for="inputname" >Name</label> | ||
| <div><input type="text" class="form-control undefined" name="name" id="inputname" > | ||
| </div></div><div class="form-group row"> | ||
| <div class="col-sm-12"> | ||
| <button type="submit" class="btn btn-primary">Save</button> | ||
| </div> | ||
| </div> | ||
| </form>`; | ||
| expect(nolines(renderForm(form, ""))).toBe(nolines(want)); | ||
| }); | ||
| it("renders a form with layout", () => { | ||
| const form = new Form({ | ||
| action: "/", | ||
| fields: [ | ||
| { | ||
| name: "name", | ||
| label: "Name", | ||
| input_type: "text" | ||
| } | ||
| ], | ||
| values: {}, | ||
| errors: {}, | ||
| layout: { | ||
| above: [ | ||
| { | ||
| type: "field", | ||
| block: false, | ||
| fieldview: "edit", | ||
| textStyle: "h2", | ||
| field_name: "name" | ||
| }, | ||
| { type: "line_break" } | ||
| ] | ||
| } | ||
| }); | ||
| const want = `<form action="/" class="form-namespace undefined" method="post" > | ||
| <input type="hidden" name="_csrf" value=""> | ||
| <span class="h2"> | ||
| <input type="text" class="form-control undefined" name="name" id="inputname" > | ||
| </span><br /></form>`; | ||
| expect(nolines(renderForm(form, ""))).toBe(nolines(want)); | ||
| }); | ||
| }); |
+111
| const { contract, is } = require("contractis"); | ||
| const { div, span, h6, text } = require("./tags"); | ||
| const { alert } = require("./layout_utils"); | ||
| const makeSegments = (body, alerts) => { | ||
| const alertsSegments = | ||
| alerts && alerts.length > 0 | ||
| ? [{ type: "blank", contents: alerts.map(a => alert(a.type, a.msg)) }] | ||
| : []; | ||
| if (typeof body === "string") | ||
| return { | ||
| above: [...alertsSegments, { type: "blank", contents: body }] | ||
| }; | ||
| else if (body.above) { | ||
| if (alerts && alerts.length > 0) body.above.unshift(alertsSegments[0]); | ||
| return body; | ||
| } else { | ||
| if (alerts && alerts.length > 0) | ||
| return { above: [...alertsSegments, body] }; | ||
| else return body; | ||
| } | ||
| }; | ||
| const render = ({ blockDispatch, layout, role, alerts }) => { | ||
| //console.log(JSON.stringify(layout, null, 2)); | ||
| function wrap(segment, isTop, ix, inner) { | ||
| if (isTop && blockDispatch && blockDispatch.wrapTop) | ||
| return blockDispatch.wrapTop(segment, ix, inner); | ||
| else | ||
| return segment.block | ||
| ? div({ class: segment.textStyle || "" }, inner) | ||
| : segment.textStyle | ||
| ? span({ class: segment.textStyle || "" }, inner) | ||
| : inner; | ||
| } | ||
| function go(segment, isTop, ix) { | ||
| if (!segment) return ""; | ||
| if (typeof segment === "string") return wrap(segment, isTop, ix, segment); | ||
| if (Array.isArray(segment)) | ||
| return wrap( | ||
| segment, | ||
| isTop, | ||
| ix, | ||
| segment.map((s, jx) => go(s, isTop, jx + ix)).join("") | ||
| ); | ||
| if (segment.minRole && role > segment.minRole) return ""; | ||
| if (segment.type && blockDispatch && blockDispatch[segment.type]) { | ||
| return wrap(segment, isTop, ix, blockDispatch[segment.type](segment, go)); | ||
| } | ||
| if (segment.type === "blank") { | ||
| return wrap(segment, isTop, ix, segment.contents); | ||
| } | ||
| if (segment.type === "card") | ||
| return wrap( | ||
| segment, | ||
| isTop, | ||
| ix, | ||
| div( | ||
| { class: "card shadow mt-4" }, | ||
| segment.title && | ||
| div( | ||
| { class: "card-header py-3" }, | ||
| h6( | ||
| { class: "m-0 font-weight-bold text-primary" }, | ||
| text(segment.title) | ||
| ) | ||
| ), | ||
| div({ class: "card-body" }, go(segment.contents)) | ||
| ) | ||
| ); | ||
| if (segment.type === "line_break") { | ||
| return "<br />"; | ||
| } | ||
| if (segment.above) { | ||
| return segment.above.map((s, ix) => go(s, isTop, ix)).join(""); | ||
| } else if (segment.besides) { | ||
| const defwidth = Math.round(12 / segment.besides.length); | ||
| const markup = div( | ||
| { class: "row" }, | ||
| segment.besides.map((t, ixb) => | ||
| div( | ||
| { | ||
| class: `col-sm-${ | ||
| segment.widths ? segment.widths[ixb] : defwidth | ||
| } text-${segment.aligns ? segment.aligns[ixb] : ""}` | ||
| }, | ||
| go(t, false, ixb) | ||
| ) | ||
| ) | ||
| ); | ||
| return isTop ? wrap(segment, isTop, ix, markup) : markup; | ||
| } else throw new Error("unknown layout segment" + JSON.stringify(segment)); | ||
| } | ||
| return go(makeSegments(layout, alerts), true, 0); | ||
| }; | ||
| const is_segment = is.obj({ type: is.maybe(is.str) }); | ||
| module.exports = contract( | ||
| is.fun( | ||
| is.obj({ | ||
| blockDispatch: is.maybe(is.objVals(is.fun(is_segment, is.str))), | ||
| layout: is.or(is_segment, is.str), | ||
| role: is.maybe(is.posint), | ||
| alerts: is.maybe(is.array(is.obj({ type: is.str, msg: is.or(is.str, is.array(is.str)) }))) | ||
| }), | ||
| is.str | ||
| ), | ||
| render | ||
| ); |
| const render = require("./layout"); | ||
| const { p } = require("./tags"); | ||
| describe("layout", () => { | ||
| it("renders a simple layout", () => { | ||
| const blockDispatch = { | ||
| wrapTop(segment, ix, s) { | ||
| return p(s); | ||
| }, | ||
| reverseIt({ theString }) { | ||
| return theString | ||
| .split("") | ||
| .reverse() | ||
| .join(""); | ||
| } | ||
| }; | ||
| const markup = { above: [{ type: "reverseIt", theString: "foobar" }] }; | ||
| expect(render({ blockDispatch, layout: markup })).toBe("<p>raboof</p>"); | ||
| }); | ||
| }); |
+6
-2
@@ -17,3 +17,6 @@ const { | ||
| module.exports = ({ options, context, action, stepName, layout }, csrfToken) => | ||
| module.exports = ( | ||
| { options, context, action, stepName, layout, mode = "show" }, | ||
| csrfToken | ||
| ) => | ||
| div( | ||
@@ -37,4 +40,5 @@ script({ src: "/builder_bundle.js" }), | ||
| ${JSON.stringify(options)}, | ||
| ${JSON.stringify(layout || {})} | ||
| ${JSON.stringify(layout || {})}, | ||
| ${JSON.stringify(mode)} | ||
| )`) | ||
| ); |
+83
-57
@@ -13,2 +13,3 @@ const { | ||
| const { contract, is } = require("contractis"); | ||
| const renderLayout = require("./layout"); | ||
@@ -172,27 +173,12 @@ const mkShowIf = sIf => | ||
| const mkFormRowForField = ( | ||
| v, | ||
| errors, | ||
| formStyle, | ||
| labelCols, | ||
| nameAdd = "" | ||
| ) => hdr => { | ||
| const innerField = (v, errors, nameAdd = "") => hdr => { | ||
| const name = hdr.name + nameAdd; | ||
| const validClass = errors[name] ? "is-invalid" : ""; | ||
| const errorFeedback = errors[name] | ||
| ? `<div class="invalid-feedback">${text(errors[name])}</div>` | ||
| : ""; | ||
| switch (hdr.input_type) { | ||
| case "fromtype": | ||
| return formRowWrap( | ||
| return displayEdit( | ||
| hdr, | ||
| displayEdit( | ||
| hdr, | ||
| name, | ||
| v && isdef(v[hdr.name]) ? v[hdr.name] : hdr.default, | ||
| validClass | ||
| ), | ||
| errorFeedback, | ||
| formStyle, | ||
| labelCols | ||
| name, | ||
| v && isdef(v[hdr.name]) ? v[hdr.name] : hdr.default, | ||
| validClass | ||
| ); | ||
@@ -207,40 +193,23 @@ case "hidden": | ||
| const opts = select_options(v, hdr); | ||
| return formRowWrap( | ||
| hdr, | ||
| `<select class="form-control ${validClass} ${ | ||
| hdr.class | ||
| }" name="${text_attr(name)}" id="input${text_attr( | ||
| name | ||
| )}">${opts}</select>`, | ||
| errorFeedback, | ||
| formStyle, | ||
| labelCols | ||
| ); | ||
| return `<select class="form-control ${validClass} ${ | ||
| hdr.class | ||
| }" name="${text_attr(name)}" id="input${text_attr( | ||
| name | ||
| )}">${opts}</select>`; | ||
| case "file": | ||
| return formRowWrap( | ||
| hdr, | ||
| `${ | ||
| v[hdr.name] ? text(v[hdr.name]) : "" | ||
| }<input type="file" class="form-control-file ${validClass} ${ | ||
| hdr.class | ||
| }" name="${text_attr(name)}" id="input${text_attr(name)}">`, | ||
| errorFeedback, | ||
| formStyle, | ||
| labelCols | ||
| ); | ||
| return `${ | ||
| v[hdr.name] ? text(v[hdr.name]) : "" | ||
| }<input type="file" class="form-control-file ${validClass} ${ | ||
| hdr.class | ||
| }" name="${text_attr(name)}" id="input${text_attr(name)}">`; | ||
| case "ordered_multi_select": | ||
| const mopts = select_options(v, hdr); | ||
| return formRowWrap( | ||
| hdr, | ||
| `<select class="form-control ${validClass} ${ | ||
| hdr.class | ||
| }" class="chosen-select" multiple name="${text_attr( | ||
| name | ||
| )}" id="input${text_attr( | ||
| name | ||
| )}">${mopts}</select><script>$(function(){$("#input${name}").chosen()})</script>`, | ||
| errorFeedback, | ||
| formStyle, | ||
| labelCols | ||
| ); | ||
| return `<select class="form-control ${validClass} ${ | ||
| hdr.class | ||
| }" class="chosen-select" multiple name="${text_attr( | ||
| name | ||
| )}" id="input${text_attr( | ||
| name | ||
| )}">${mopts}</select><script>$(function(){$("#input${name}").chosen()})</script>`; | ||
@@ -266,6 +235,44 @@ default: | ||
| : the_input; | ||
| return formRowWrap(hdr, inner, errorFeedback, formStyle, labelCols); | ||
| return inner; | ||
| } | ||
| }; | ||
| const mkFormRowForField = ( | ||
| v, | ||
| errors, | ||
| formStyle, | ||
| labelCols, | ||
| nameAdd = "" | ||
| ) => hdr => { | ||
| const name = hdr.name + nameAdd; | ||
| const errorFeedback = errors[name] | ||
| ? `<div class="invalid-feedback">${text(errors[name])}</div>` | ||
| : ""; | ||
| if (hdr.input_type === "hidden") { | ||
| return innerField(v, errors, nameAdd)(hdr); | ||
| } else | ||
| return formRowWrap( | ||
| hdr, | ||
| innerField(v, errors, nameAdd)(hdr), | ||
| errorFeedback, | ||
| formStyle, | ||
| labelCols | ||
| ); | ||
| }; | ||
| const renderFormLayout = form => { | ||
| const blockDispatch = { | ||
| field(segment) { | ||
| const field = form.fields.find(f => f.name === segment.field_name); | ||
| return innerField(form.values, form.errors)(field); | ||
| }, | ||
| action({ action_name }) { | ||
| return `<button type="submit" class="btn btn-primary">${text( | ||
| form.submitLabel || "Save" | ||
| )}</button>`; | ||
| } | ||
| }; | ||
| return renderLayout({ blockDispatch, layout: form.layout }); | ||
| }; | ||
| const renderForm = (form, csrfToken) => { | ||
@@ -298,5 +305,24 @@ if (form.isStateForm) { | ||
| ); | ||
| } else return mkForm(form, csrfToken, form.errors); | ||
| } else if (form.layout) return mkFormWithLayout(form, csrfToken); | ||
| else return mkForm(form, csrfToken, form.errors); | ||
| }; | ||
| const mkFormWithLayout = (form, csrfToken) => { | ||
| const hasFile = form.fields.some(f => f.input_type === "file"); | ||
| const csrfField = `<input type="hidden" name="_csrf" value="${csrfToken}">`; | ||
| const top = `<form action="${form.action}" class="form-namespace ${ | ||
| form.class | ||
| }" method="${form.methodGET ? "get" : "post"}" ${ | ||
| hasFile ? 'encType="multipart/form-data"' : "" | ||
| }>`; | ||
| const blurbp = form.blurb ? p(text(form.blurb)) : ""; | ||
| const hiddens = form.fields | ||
| .filter(f => f.input_type === "hidden") | ||
| .map(f => innerField(form.values, form.errors)(f)) | ||
| .join(""); | ||
| return ( | ||
| blurbp + top + csrfField + hiddens + renderFormLayout(form) + "</form>" | ||
| ); | ||
| }; | ||
| const mkForm = (form, csrfToken, errors = {}) => { | ||
@@ -303,0 +329,0 @@ const hasFile = form.fields.some(f => f.input_type === "file"); |
+1
-1
@@ -121,3 +121,3 @@ const { | ||
| domReady(`$(window).scroll(function () { | ||
| if ($(window).scrollTop() >= 50) { | ||
| if ($(window).scrollTop() >= 10) { | ||
| $('.navbar').css('background','white'); | ||
@@ -124,0 +124,0 @@ } else { |
+3
-3
| { | ||
| "name": "@saltcorn/markup", | ||
| "version": "0.0.8", | ||
| "version": "0.0.9", | ||
| "description": "Markup for Saltcorn, open-source no-code platform", | ||
@@ -13,3 +13,3 @@ "homepage": "https://saltcorn.com", | ||
| "dependencies": { | ||
| "contractis": "^0.0.8", | ||
| "contractis": "^0.0.9", | ||
| "escape-html": "^1.0.3", | ||
@@ -28,3 +28,3 @@ "xss": "^1.0.6" | ||
| }, | ||
| "gitHead": "d4a0f3070191b628424af5325c229a0c82681de7" | ||
| "gitHead": "2a088e28b3df98fb79b91d891ae0c2bff9811300" | ||
| } |
29861
30.41%13
30%984
28.46%+ Added
- Removed
Updated