Libraryless. Click here for Pure Java version (19256L/131K).
// one instance should only be used for one page at a time sclass HCRUD extends HAbstractRenderable { HCRUD_Data data; int defaultTextFieldCols = 80; // yeah these fields are a mess... bool mutationRights = true; bool allowCreateOrDelete = true; bool allowCreate = true; bool allowEdit = true; bool singleton; bool cmdsLeft; // put commands on the left instead of the right S tableClass; // CSS class for table S formTableClass; // CSS class for form Set<S> unshownFields; // not shown in table or form Set<S> uneditableFields; // not shown in form Set<S> unlistedFields; // not shown in table bool showCheckBoxes; bool haveJQuery, haveSelectizeJS; bool needsJQuery; // unused bool paginate; bool sortable; bool buttonsOnTop, buttonsOnBottom = true; S formID = "crudForm"; bool showQuickSaveButton; // needs hnotificationPopups() SS params; new HTMLPaginator paginator; // sort options S sortByField; S sortParameter = "sort"; bool descending; int flexibleLengthListLeeway = 5; // how many empty rows to display at the end of each flexible length list // set internally after update/create or from "selectObj" param to highlight an item in the list (or show only that item) O objectIDToHighlight; S fieldPrefix = "f_", listFieldPrefix = "fList_"; *() {} *(HCRUD_Data *data) {} *(S *baseLink, HCRUD_Data *data) {} S newLink() { ret appendQueryToURL(baseLink, cmd := "new"); } S deleteLink(O id) { ret appendQueryToURL(baseLink, "delete_" + id, 1); } S editLink(O id) { ret appendQueryToURL(baseLink, edit := id); } S duplicateLink(O id) { ret appendQueryToURL(baseLink, duplicate := id); } // also handles commands if withCmds=true // you probably want to call renderPage() instead to handle all commands S render(bool withCmds, SS params) { this.params = params; if (objectIDToHighlight == null) objectIDToHighlight = params.get("selectObj"); if (!withCmds) ret renderTable(false); try answer handleCommands(params); ret renderMsgs(params) + pUnlessEmpty(nav()) + renderTable(withCmds); } swappable S nav() { new LS l; if (actuallyAllowCreate()) l.add(ahref(newLink(), "New " + itemName())); ret joinWithVBar(l); } S handleCommands(SS params) { new LS msgs; if (eqGet(params, "action", "create")) { if (!actuallyAllowCreate()) fail("Creating objects not allowed"); O id = data.createObject(subMapStartingWith_dropPrefix(params, fieldPrefix)); msgs.add(itemName() + " created (ID: " + id + ")"); objectIDToHighlight = id; } if (eqGet(params, "action", "update")) { if (!actuallyAllowEdit()) fail("Editing objects not allowed"); S id = params.get("id"); msgs.add(data.updateObject(id, subMapStartingWith_dropPrefix(params, fieldPrefix))); objectIDToHighlight = id; } LS toDeleteList = keysDeprefixNemptyValue(params, "delete_"); if (eq(params.get("bulkAction"), "deleteSelected")) toDeleteList.addAll(keysDeprefixNemptyValue(params, "obj_")); for (S toDelete : toDeleteList) { if (!actuallyAllowDelete()) fail("Deleting objects not allowed"); msgs.add(data.deleteObject(toDelete)); } ret empty(msgs) ? "" : refreshAfterCommand(params, msgs); } swappable S refreshAfterCommand(SS params, LS msgs) { ret refreshWithMsgs(msgs, anchor := objectIDToHighlight == null ? null : "obj" + objectIDToHighlight, params := objectIDToHighlight == null ? null : litmap(selectObj := objectIDToHighlight)); } S encodeField(S s) { ret or(data.fieldNameToHTML(s), s); } // in table swappable S renderValue(S field, O value) { if (value cast HTML) ret value.html; value = deref(value); if (value cast SecretValue) ret hhiddenStuff(renderValue_inner(value!)); ret renderValue_inner(value); } swappable S renderValue_inner(O value) { if (value cast Bool) ret yesNo_short(value); ret htmlEncode_nlToBr_withIndents(strOrEmpty(value)); } S renderTable(bool withCmds) { ret renderTable(withCmds, data.list()); } S valueToSortable(O value) { if (value cast HTML) ret value!; ret strOrNull(value); } // l = list of maps as it comes from data object S renderTable(bool withCmds, L<MapSO> l) { if (empty(l)) ret p("No entries"); if (nempty(sortByField)) l = sortByTransformedMapKey_alphaNum valueToSortable(l, sortByField); if (descending) l = reversed(l); // l2 is the rendered map. use keyEncoding to get from l keys to l2 keys new SS keyEncoding; L<MapSO> l2 = map(l, map -> { O id = itemID(map); map = mapMinusKeys(map, joinSets(unshownFields, unlistedFields)); MapSO map2 = postProcessTableRow(map, mapToMap( (key, value) -> pair(mapPut_returnValue(keyEncoding, key, encodeField(key)), renderValue(key, value)), map)); if (singleton) map2.remove(encodeField(data.idField())); // don't show ID in table in singleton mode if (withCmds) map2 = addCmdsToTableRow(map, map2); // add anchor to row map2.put(firstKey(map2), aname("obj" + id, firstValue(map2))); ret map2; }); new LS out; if (paginate) { paginator.processParams(params); paginator.baseLink = baseLink; paginator.max = l(l2); out.add(pUnlessEmpty(paginator.renderNav())); L<MapSO> l3 = subListOrFull(l2, paginator.visibleRange()); // if highlighted object is not on page, show only this object if (objectIDToHighlight != null && !any isHighlighted(l3)) l3 = llNonNulls(firstThat isHighlighted(l2)); l2 = l3; } new SS replaceHeaders; if (sortable && !singleton) for (S key, html : keyEncoding) { bool sortedByField = eq(sortByField, key); bool showDescendingLink = sortedByField && !descending; S htmlOld = html; if (sortedByField) { S title = showDescendingLink ? "Click here to sort descending" : "Click here to sort ascending"; S titleSorted = "Sorted by this field (" + (descending ? "descending" : "ascending") + ")"; title = titleSorted + ". " + title; html = span_title(title, unicode_downOrUpPointingTriangle(descending)) + " " + html; } S sortLink = appendQueryToURL(baseLink, sortParameter, showDescendingLink ? "-" + key : key); replaceHeaders.put(htmlOld, /*html + " " + ahref(sortLink, unicode_smallDownOrUpPointingTriangle(showDescendingLink), +title)*/ ahref(sortLink, html)); } out.add(hpostform( htmlTable2_noHtmlEncode(l2, paramsPlus(tableParams(), +replaceHeaders)) + (!withCmds || !showCheckBoxes ? "" : "\n" + pUnlessEmpty(renderBulkCmds())), action := baseLink)); ret lines_rtrim(out); } // item is after encodeField bool isHighlighted(MapSO item) { O id = item.get(encodeField(idField())); //print("isHighlighted ID: " + toStringWithClass(id) + " / " + toStringWithClass(objectIDToHighlight)); ret eq(id, objectIDToHighlight); } swappable S renderBulkCmds() { ret "Bulk action: " + hselect("bulkAction", litorderedmap("" := "", "deleteSelected" := "Delete selected")) + " " + hsubmit("OK", onclick := "return confirm('Are you sure?')"); } MapSO addCmdsToTableRow(MapSO map, MapSO map2) { if (showCheckBoxes) { O id = itemID(map); map2.put(checkBoxKey(), hcheckbox("obj_" + id, false, title := "Select this object for a bulk action")); map2 = putKeysFirst(map2, checkBoxKey()); } map2.put(cmdsKey(), renderCmds(map)); if (cmdsLeft) map2 = putKeysFirst(map2, cmdsKey()); ret map2; } /*swappable*/ O[] tableParams() { ret litparams( tdParams := litparams(valign := "top"), tableParams := litparams(class := tableClass)); } S renderForm(MapSO map) { temp tempSetTL(htmlencode_forParams_useV2, true); //print("renderForm: filteredFields=" + data.filteredFields()); map = mapMinusKeys(map, joinSets(unshownFields, uneditableFields, data.filteredFields())); MapSO mapWithoutID = mapWithoutKey(map, data.idField()); LLS matrix = map(mapWithoutID, (field, value) -> { S help = data.fieldHelp(field); ret ll( encodeField(field), addHelpText(help, renderInput(field, value)) ); }); massageFormMatrix(map, matrix); ret htableRaw_valignTop(matrix , empty(formTableClass) ? litparams(border := 1, cellpadding := 4) : litparams(class := formTableClass)); } S renderInput(S field, O value) { S name = fieldPrefix + field; ret renderInput(name, data.getRenderer(field), value); } S renderInput(S name, HCRUD_Data.Renderer r, O value) { //print("Renderer for " + name + ": " + r); if (r != null) value = r.preprocessValue(value); // switch by renderer type if (r cast HCRUD_Data.AceEditor) ret hAceEditor(strOrEmpty(value), style := "width: " + r.cols + "ch; height: " + r.rows + "em", +name); if (r cast HCRUD_Data.TextArea) ret htextarea(strOrEmpty(value), +name, cols := r.cols, rows := r.rows); if (r cast HCRUD_Data.TextField) ret htextfield(name, strOrEmpty(value), size := r.cols, style := "font-family: monospace"); if (r cast HCRUD_Data.ComboBox) ret renderComboBox(name, r.valueToEntry(value), r.entries, r.editable); if (r cast HCRUD_Data.DynamicComboBox) ret renderDynamicComboBox(name, r.valueToEntry(value), r.info, r.editable); if (r cast HCRUD_Data.CheckBox) //ret hcheckbox(name, isTrue(value)); ret htrickcheckboxWithText(name, "", isTrue(value)); if (r cast HCRUD_Data.FlexibleLengthList) { L list = cast value; new LS rows; int n = l(list)+flexibleLengthListLeeway; for i to n: { O item = _get(list, i); //print("Item: " + item); rows.add(tr(td(i+1 + ".", align := "right") + td(renderInput(name + "_" + i, r.itemRenderer, item)))); } ret htag table(lines(rows)); } ret renderInput_default(name, value); } S renderInput_default(S name, O value) { ret htextfield(name, strOrEmpty(value), size := defaultTextFieldCols); } S renderNewForm() { ret renderNewForm(data.emptyObject()); } // pre-populate fields from request parameters S renderNewFormWithParams(SS params) { MapSO map = joinMaps(data.emptyObject(), (Map) subMapStartingWith_dropPrefix(params, fieldPrefix)); // pre-populate list fields for (S key, value : subMapStartingWith_dropPrefix(params, listFieldPrefix)) { LS l = splitAt(key, "_"); if (l(l) == 2) { S field = first(l); int idx = parseInt(second(l)); LS list = cast map.get(field); if (!list instanceof ArrayList) map.put(field, list = new L); listPut(list, idx, value); } } ret renderNewForm(map); } S renderNewForm(MapSO map) { //printStruct("renderNewForm", map); S buttons = p(hsubmit("Create")); ret hpostform( hhidden("action", "create") + stringIf(buttonsOnTop, buttons) + renderForm(map) + stringIf(buttonsOnBottom, buttons), paramsPlus(formParameters(), action := baseLink)); } swappable O[] formParameters() { ret litparams(id := formID); } S idField() { ret data.idField(); } S renderEditForm(S id) { MapSO map = data.getObject(id); if (map == null) ret htmlEncode2("Entry " + id + " not found"); S onlyFields = mapGet(params, "onlyFields"); if (nempty(onlyFields)) map = onlyKeys(map, itemPlus(idField(), tok_identifiersOnly(onlyFields))); S buttons = p_vbar( hsubmit("Save changes"), !showQuickSaveButton ? "" : hbuttonOnClick_noSubmit("Save & keep editing", [[ $.ajax({ type: 'POST', url: $('#crudForm').attr('action'), data: $('#crudForm').serialize(), success: function(response) { successNotification("Saved"); }, }).error(function() { errorNotification("Couldn't save"); }); ]]), deleteObjectHTML(id)); ret hpostform( hhidden("action", "update") + hhidden(+id) + p("Object ID: " + htmlEncode2(id)) + + stringIf(buttonsOnTop, buttons) + renderForm(map) + stringIf(buttonsOnBottom, buttons), paramsPlus(formParameters(), action := baseLink + "#obj" + id)); } S renderPage(SS params) { this.params = params; try answer handleComboSearch(params); if (eqGet(params, "cmd", "new")) ret frame("New " + itemName(), renderNewFormWithParams(params)); if (nempty(params.get("edit"))) ret frame("Edit " + itemName(), renderEditForm(params.get("edit"))); if (nempty(params.get("duplicate"))) ret frame("New " + itemName(), renderNewForm(data.getObjectForDuplication(params.get("duplicate")))); // handle commands, render list ret frame(ahref(baseLink, firstToUpper(singleton ? data.itemName() : data.itemNamePlural())), render(mutationRights, params)); } HCRUD makeFrame(MakeFrame makeFrame) { super.makeFrame(makeFrame); this; } S cmdsKey() { ret "<!-- cmds -->"; } S checkBoxKey() { ret "<!-- checkbox -->"; } S itemName() { ret data.itemName(); } swappable MapSO postProcessTableRow(MapSO data, MapSO rendered) { ret rendered; } O itemID(MapSO item) { ret mapGet(item, data.idField()); } swappable S renderCmds(MapSO item) { O id = itemID(item); ret joinNemptiesWithVBar( !actuallyAllowEdit() ? null : ahref(editLink(id), "EDIT"), deleteObjectHTML(id), !actuallyAllowCreate() ? null : ahref(duplicateLink(id), "dup", title := "duplicate") ); } bool actuallyAllowCreate() { ret !singleton && allowCreateOrDelete && allowCreate; } bool actuallyAllowEdit() { ret allowCreateOrDelete && allowEdit; } bool actuallyAllowDelete() { ret !singleton && allowCreateOrDelete; } // e.g. for adding rows to edit/create form swappable void massageFormMatrix(MapSO map, LLS matrix) { } S renderComboBox(S name, S value, LS entries, bool editable) { if (haveSelectizeJS) { // coolest option - use selectize.js S id = aGlobalID(); ret hselect_list(entries, value, +name, +id) + hjs("$('#" + id + "').selectize" + [[ ({ searchField: 'text', openOnFocus: true, dropdownParent: 'body', create: ]] + jsBool(editable) + [[ /*allowEmptyOption: true*/ ]] + moreSelectizeOptions(name) + [[ }); ]]) // dirty CSS quick-fix (TODO: send only once) + hcss(".selectize-input { min-width: 300px }"); } if (haveJQuery) { // make searchable list if JQuery is available // (functional, but looks quite poor on Firefox) // TODO: this seems to always be editable S id = aGlobalID(); ret tag datalist(mapToLines hoption(entries), +id) + tag input("", +name, list := id); } // standard non-searchable list if (editable) ret hinputfield(name, value); // no editable combo box possible without selectize.js ret hselect_list(entries, value, +name); } // dev. S renderDynamicComboBox(S name, S value, S info, bool editable) { assertTrue(+haveSelectizeJS); S id = aGlobalID(); ret hselect_list(llNempties(value), value, +name, +id) + hjs("$('#" + id + "').selectize" + [[ ({ searchField: 'text', valueField: 'text', labelField: 'text', openOnFocus: true, dropdownParent: 'body', create: ]] + jsBool(editable) + [[, load: function(query, callback) { if (!query.length) return callback(); console.log("Loading " + ]] + jsQuote(baseLink) + [[); $.ajax({ url: ]] + jsQuote(baseLink) + [[, type: 'GET', dataType: 'json', data: { comboSearchInfo: ]] + jsQuote(info) + [[, comboSearchQuery: query }, error: function() { console.log("Got error"); callback(); }, success: function(res) { //console.log("Got data: " + res); var converted = res.map(x => { return {text: x}; }); //console.log("Converted: " + converted); callback(converted); } }); } /*allowEmptyOption: true*/ ]] + moreSelectizeOptions(name) + [[ }); ]]) // dirty CSS quick-fix (TODO: send only once) + hcss(".selectize-input { min-width: 300px }"); } void processSortParameter(SS params) { S sort = mapGet(params, sortParameter); sortByField = null; descending = false; if (nempty(sort)) if (startsWith(sort, "-")) { descending = true; sortByField = substring(sort, 1); } else sortByField = sort; } S deleteObjectHTML(O id) { ret !actuallyAllowDelete() ? null : !data.objectCanBeDeleted(id) ? span_title("Object can't be deleted, please delete references first", htmlEncode2(unicode_DEL())) : ahrefWithConfirm( "Really delete item " + id + "?", deleteLink(id), htmlEncode2(unicode_DEL()), title := "delete"); } // helpText is also HTML S addHelpText(S helpText, S html) { ret empty(helpText) ? html : html + p(small(helpText), style := "text-align: right"); } swappable S moreSelectizeOptions(S name) { ret ""; } // deliver content for dynamic search in combo boxes S handleComboSearch(SS params) { S query = params.get("comboSearchQuery"); if (nempty(query)) { S info = params.get("comboSearchInfo"); ret jsonEncode_shallowLineBreaks(data.comboBoxSearch(info, query)); } null; } }
download show line numbers debug dex
Travelled to 7 computer(s): bhatertpkbcr, mqqgnosmbjvj, onxytkatvevr, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1026001 |
Snippet name: | HCRUD - CRUD in HTML with pluggable data handler |
Eternal ID of this version: | #1026001/210 |
Text MD5: | fd847eaf664ee12b7661c1517bd9e581 |
Transpilation MD5: | bcd8cdfa137ad9c221f6fa34b2625311 |
Author: | stefan |
Category: | javax / html |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2021-01-17 02:29:37 |
Source code size: | 18822 bytes / 544 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 641 / 1633 |
Version history: | 209 change(s) |
Referenced in: | [show references] |
Formerly at http://tinybrain.de/1026001 & http://1026001.tinybrain.de