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

390
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 (16841L/119K).

// 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;
  
  SS params;
  new HTMLPaginator paginator;
  
  // sort options
  S sortByField;
  S sortParameter = "sort";
  bool descending;
  
  // set internally after update/create
  O objectIDToHighlight;
  
  *() {}
  *(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 (!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, "f_"));
      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, "f_")));
      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 nempty(msgs) ? refreshWithMsgs(msgs, anchor := objectIDToHighlight != null ? "obj" + objectIDToHighlight : null) : "";
  }
  
  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(data.fieldNameToHTML(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()));
      
      //if (objectIDToHighlight != null) // TODO: scroll there
      
      l2 = subListOrFull(l2, paginator.visibleRange());
    }
    
    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);
  }
  
  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) {
    map = mapMinusKeys(map, joinSets(unshownFields, uneditableFields));
    LLS matrix = map(map, (field, value) -> {
      S help = data.fieldHelp(field);
      ret ll(
        encodeField(field),
        renderInput(field, value)
          + (empty(help) ? "" : p(small(help), style := "text-align: right"))
      );
    });
    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 = "f_" + 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.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);
      
    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 leeway = 5, n = l(list)+leeway;
      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());
  }
  
  S renderNewForm(MapSO map1) {
    //printStruct("renderNewForm", map1);
    Map<S, O> map = mapWithoutKey(map1, data.idField());
    //printStruct("renderNewForm", map);
    ret hpostform(
      hhidden("action", "create") +
      renderForm(map)
      + p(hsubmit("Create")),
      paramsPlus(formParameters(), action := baseLink));
  }
  
  swappable O[] formParameters() { null; }
  
  S renderEditForm(S id) {
    Map<S, O> map = mapWithoutKey(data.getObject(id), data.idField());
    if (map == null) ret htmlEncode2("Entry " + id + " not found");
    ret hpostform(
      hhidden("action", "update") +
      hhidden(+id) +
      p("Object ID: " + htmlEncode2(id)) +
      renderForm(map)
      + p(hsubmit("Save changes")),
      paramsPlus(formParameters(), action := baseLink + "#obj" + id));
  }
  
  S renderPage(SS params) {
    if (eqGet(params, "cmd", "new"))
      ret frame("New " + itemName(), renderNewForm());
    if (nempty(params.get("edit")))
      ret frame("Edit " + itemName(), renderEditForm(params.get("edit")));
    if (nempty(params.get("duplicate")))
      ret frame("New " + itemName(), renderNewForm(data.getObject(params.get("duplicate"))));
    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"),
      !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"),
      !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) {
  }
  
  swappable S renderComboBox(S name, S value, LS entries) {
    if (haveSelectizeJS) {
      // coolest option - use selectize.js
      S id = aGlobalID();
      ret hselect_list(entries, value, +name, +id)
        + hjs("$('#" + id + "').selectize" + [[
          ({
            searchField: 'text',
            openOnFocus: true
          });
        ]])
        // 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)
      S id = aGlobalID();
      ret tag datalist(mapToLines hoption(entries), +id)
        + tag input("", +name, list := id);
    }
    
    // standard non-searchable list
    ret hselect_list(entries, value, +name);
  }
  
  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;
  }
}

download  show line numbers  debug dex   

Travelled to 6 computer(s): bhatertpkbcr, mqqgnosmbjvj, onxytkatvevr, 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/140
Text MD5: 99a7ddc21e808c234ead3b42d7810839
Transpilation MD5: e1d9c0d374bdf3f215b311fe38e0fc8c
Author: stefan
Category: javax / html
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-09-21 15:26:27
Source code size: 13173 bytes / 390 lines
Pitched / IR pitched: No / No
Views / Downloads: 445 / 1195
Version history: 139 change(s)
Referenced in: #1003674 #1026135

Formerly at http://tinybrain.de/1026001 & http://1026001.tinybrain.de