Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

544
LINES

< > BotCompany Repo | #1026001 - HCRUD - CRUD in HTML with pluggable data handler

JavaX fragment (include) [tags: use-pretranspiled]

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