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).

1  
// one instance should only be used for one page at a time
2  
sclass HCRUD extends HAbstractRenderable {
3  
  HCRUD_Data data;
4  
  int defaultTextFieldCols = 80;
5  
  
6  
  // yeah these fields are a mess...
7  
  bool mutationRights = true;
8  
  bool allowCreateOrDelete = true;
9  
  bool allowCreate = true;
10  
  bool allowEdit = true;
11  
  bool singleton;
12  
  
13  
  bool cmdsLeft; // put commands on the left instead of the right
14  
  S tableClass; // CSS class for table
15  
  S formTableClass; // CSS class for form
16  
  Set<S> unshownFields; // not shown in table or form
17  
  Set<S> uneditableFields; // not shown in form
18  
  Set<S> unlistedFields; // not shown in table
19  
  bool showCheckBoxes;
20  
  
21  
  bool haveJQuery, haveSelectizeJS;
22  
  bool needsJQuery; // unused
23  
  bool paginate;
24  
  bool sortable;
25  
  
26  
  SS params;
27  
  new HTMLPaginator paginator;
28  
  
29  
  // sort options
30  
  S sortByField;
31  
  S sortParameter = "sort";
32  
  bool descending;
33  
  
34  
  // set internally after update/create
35  
  O objectIDToHighlight;
36  
  
37  
  *() {}
38  
  *(HCRUD_Data *data) {}
39  
  *(S *baseLink, HCRUD_Data *data) {}
40  
  
41  
  S newLink() { ret appendQueryToURL(baseLink, cmd := "new"); }
42  
  S deleteLink(O id) { ret appendQueryToURL(baseLink, "delete_" + id, 1); }
43  
  S editLink(O id) { ret appendQueryToURL(baseLink, edit := id); }
44  
  S duplicateLink(O id) { ret appendQueryToURL(baseLink, duplicate := id); }
45  
  
46  
  // also handles commands if withCmds=true
47  
  // you probably want to call renderPage() instead to handle all commands
48  
  S render(bool withCmds, SS params) {
49  
    this.params = params;
50  
    if (!withCmds) ret renderTable(false);
51  
    
52  
    try answer handleCommands(params);
53  
    
54  
    ret renderMsgs(params)
55  
      + pUnlessEmpty(nav())
56  
      + renderTable(withCmds);
57  
  }
58  
  
59  
  swappable S nav() {
60  
    new LS l;
61  
    if (actuallyAllowCreate())
62  
      l.add(ahref(newLink(), "New " + itemName()));
63  
    ret joinWithVBar(l);
64  
  }
65  
66  
  S handleCommands(SS params) {
67  
    new LS msgs;
68  
    
69  
    if (eqGet(params, "action", "create")) {
70  
      if (!actuallyAllowCreate()) fail("Creating objects not allowed");
71  
      O id = data.createObject(subMapStartingWith_dropPrefix(params, "f_"));
72  
      msgs.add(itemName() + " created (ID: " + id + ")");
73  
      objectIDToHighlight = id;
74  
    }
75  
    
76  
    if (eqGet(params, "action", "update")) {
77  
      if (!actuallyAllowEdit()) fail("Editing objects not allowed");
78  
      S id = params.get("id");
79  
      msgs.add(data.updateObject(id,
80  
        subMapStartingWith_dropPrefix(params, "f_")));
81  
      objectIDToHighlight = id;
82  
    }
83  
84  
    LS toDeleteList = keysDeprefixNemptyValue(params, "delete_");
85  
    if (eq(params.get("bulkAction"), "deleteSelected"))
86  
      toDeleteList.addAll(keysDeprefixNemptyValue(params, "obj_"));
87  
      
88  
    for (S toDelete : toDeleteList) {
89  
      if (!actuallyAllowDelete()) fail("Deleting objects not allowed");
90  
      msgs.add(data.deleteObject(toDelete));
91  
    }
92  
93  
    ret nempty(msgs) ? refreshWithMsgs(msgs, anchor := objectIDToHighlight != null ? "obj" + objectIDToHighlight : null) : "";
94  
  }
95  
  
96  
  S encodeField(S s) {
97  
    ret or(data.fieldNameToHTML(s), s);
98  
  }
99  
  
100  
  // in table
101  
  swappable S renderValue(S field, O value) {
102  
    if (value cast HTML) ret value.html;
103  
    value = deref(value);
104  
    if (value cast SecretValue)
105  
      ret hhiddenStuff(renderValue_inner(value!));
106  
    ret renderValue_inner(value);
107  
  }
108  
109  
  swappable S renderValue_inner(O value) {  
110  
    if (value cast Bool)
111  
      ret yesNo_short(value);
112  
    ret htmlEncode_nlToBr_withIndents(strOrEmpty(value));
113  
  }
114  
  
115  
  S renderTable(bool withCmds) {
116  
    ret renderTable(withCmds, data.list());
117  
  }
118  
  
119  
  S valueToSortable(O value) {
120  
    if (value cast HTML)
121  
      ret value!;
122  
    ret strOrNull(value);
123  
  }
124  
  
125  
  // l = list of maps as it comes from data object
126  
  S renderTable(bool withCmds, L<MapSO> l) {
127  
    if (empty(l)) ret p("No entries");
128  
    if (nempty(sortByField)) l = sortByTransformedMapKey_alphaNum valueToSortable(l, sortByField);
129  
    if (descending) l = reversed(l);
130  
    
131  
    // l2 is the rendered map. use keyEncoding to get from l keys to l2 keys
132  
    new SS keyEncoding;
133  
    L<MapSO> l2 = map(l, map -> {
134  
      O id = itemID(map);
135  
      map = mapMinusKeys(map, joinSets(unshownFields, unlistedFields));
136  
      MapSO map2 = postProcessTableRow(map, mapToMap(
137  
        (key, value) -> pair(mapPut_returnValue(keyEncoding, key, encodeField(key)), renderValue(key, value)),
138  
        map));
139  
      if (singleton)
140  
        map2.remove(data.fieldNameToHTML(data.idField())); // don't show ID in table in singleton mode
141  
      if (withCmds)
142  
        map2 = addCmdsToTableRow(map, map2);
143  
      // add anchor to row
144  
      map2.put(firstKey(map2), aname("obj" + id, firstValue(map2)));
145  
      ret map2;
146  
    });
147  
    
148  
    new LS out;
149  
    
150  
    if (paginate) {
151  
      paginator.processParams(params);
152  
      paginator.baseLink = baseLink;
153  
      paginator.max = l(l2);
154  
      out.add(pUnlessEmpty(paginator.renderNav()));
155  
      
156  
      //if (objectIDToHighlight != null) // TODO: scroll there
157  
      
158  
      l2 = subListOrFull(l2, paginator.visibleRange());
159  
    }
160  
    
161  
    new SS replaceHeaders;
162  
    if (sortable && !singleton)
163  
      for (S key, html : keyEncoding) {
164  
        bool sortedByField = eq(sortByField, key);
165  
        bool showDescendingLink = sortedByField && !descending;
166  
        S htmlOld = html;
167  
        if (sortedByField) {
168  
          S title = showDescendingLink ? "Click here to sort descending" : "Click here to sort ascending";
169  
          S titleSorted = "Sorted by this field ("
170  
            + (descending ? "descending" : "ascending") + ")";
171  
          title = titleSorted + ". " + title;
172  
          html = span_title(title, unicode_downOrUpPointingTriangle(descending)) + " " + html;
173  
        }
174  
        S sortLink = appendQueryToURL(baseLink, sortParameter, showDescendingLink ? "-" + key : key);
175  
        replaceHeaders.put(htmlOld,
176  
          /*html + " " + ahref(sortLink,
177  
            unicode_smallDownOrUpPointingTriangle(showDescendingLink), +title)*/
178  
          ahref(sortLink, html));
179  
      }
180  
181  
    out.add(hpostform(
182  
      htmlTable2_noHtmlEncode(l2, paramsPlus(tableParams(), +replaceHeaders))
183  
        + (!withCmds || !showCheckBoxes ? "" : "\n" + pUnlessEmpty(renderBulkCmds())),
184  
      action := baseLink));
185  
      
186  
    ret lines_rtrim(out);
187  
  }
188  
  
189  
  swappable S renderBulkCmds() {
190  
    ret "Bulk action: " + hselect("bulkAction", litorderedmap("" := "", "deleteSelected" := "Delete selected"))
191  
      + " " + hsubmit("OK", onclick := "return confirm('Are you sure?')");
192  
  }
193  
  
194  
  MapSO addCmdsToTableRow(MapSO map, MapSO map2) {
195  
    if (showCheckBoxes) {
196  
      O id = itemID(map);
197  
      map2.put(checkBoxKey(), hcheckbox("obj_" + id, false, title := "Select this object for a bulk action"));
198  
      map2 = putKeysFirst(map2, checkBoxKey());
199  
    }
200  
    map2.put(cmdsKey(), renderCmds(map));
201  
    if (cmdsLeft)
202  
      map2 = putKeysFirst(map2, cmdsKey());
203  
    ret map2;
204  
  }
205  
  
206  
  /*swappable*/ O[] tableParams() {
207  
    ret litparams(
208  
      tdParams := litparams(valign := "top"),
209  
      tableParams := litparams(class := tableClass));
210  
  }
211  
  
212  
  S renderForm(MapSO map) {
213  
    map = mapMinusKeys(map, joinSets(unshownFields, uneditableFields));
214  
    LLS matrix = map(map, (field, value) -> {
215  
      S help = data.fieldHelp(field);
216  
      ret ll(
217  
        encodeField(field),
218  
        renderInput(field, value)
219  
          + (empty(help) ? "" : p(small(help), style := "text-align: right"))
220  
      );
221  
    });
222  
    massageFormMatrix(map, matrix);
223  
    
224  
    ret htableRaw_valignTop(matrix
225  
      , empty(formTableClass) ? litparams(border := 1, cellpadding := 4) : litparams(class := formTableClass));
226  
  }
227  
  
228  
  S renderInput(S field, O value) {
229  
    S name = "f_" + field;
230  
    ret renderInput(name, data.getRenderer(field), value);
231  
  }
232  
  
233  
  S renderInput(S name, HCRUD_Data.Renderer r, O value) {
234  
    //print("Renderer for " + name + ": " + r);
235  
    if (r != null) value = r.preprocessValue(value);
236  
    
237  
    // switch by renderer type
238  
    
239  
    if (r cast HCRUD_Data.TextArea)
240  
      ret htextarea(strOrEmpty(value), +name, cols := r.cols, rows := r.rows);
241  
      
242  
    if (r cast HCRUD_Data.TextField)
243  
      ret htextfield(name, strOrEmpty(value), size := r.cols, style := "font-family: monospace");
244  
      
245  
    if (r cast HCRUD_Data.ComboBox)
246  
      ret renderComboBox(name, r.valueToEntry(value), r.entries);
247  
      
248  
    if (r cast HCRUD_Data.CheckBox)
249  
      //ret hcheckbox(name, isTrue(value));
250  
      ret htrickcheckboxWithText(name, "", isTrue(value));
251  
      
252  
    if (r cast HCRUD_Data.FlexibleLengthList) {
253  
      L list = cast value;
254  
      new LS rows;
255  
      
256  
      int leeway = 5, n = l(list)+leeway;
257  
      for i to n: {
258  
        O item = _get(list, i);
259  
        print("Item: " + item);
260  
        rows.add(tr(td(i+1 + ".", align := "right")
261  
          + td(renderInput(name + "_" + i, r.itemRenderer, item))));
262  
      }
263  
      ret htag table(lines(rows));
264  
    }
265  
      
266  
    ret renderInput_default(name, value);
267  
  }
268  
  
269  
  S renderInput_default(S name, O value) {
270  
    ret htextfield(name, strOrEmpty(value), size := defaultTextFieldCols);
271  
  }
272  
  
273  
  S renderNewForm() {
274  
    ret renderNewForm(data.emptyObject());
275  
  }
276  
  
277  
  S renderNewForm(MapSO map1) {
278  
    //printStruct("renderNewForm", map1);
279  
    Map<S, O> map = mapWithoutKey(map1, data.idField());
280  
    //printStruct("renderNewForm", map);
281  
    ret hpostform(
282  
      hhidden("action", "create") +
283  
      renderForm(map)
284  
      + p(hsubmit("Create")),
285  
      paramsPlus(formParameters(), action := baseLink));
286  
  }
287  
  
288  
  swappable O[] formParameters() { null; }
289  
  
290  
  S renderEditForm(S id) {
291  
    Map<S, O> map = mapWithoutKey(data.getObject(id), data.idField());
292  
    if (map == null) ret htmlEncode2("Entry " + id + " not found");
293  
    ret hpostform(
294  
      hhidden("action", "update") +
295  
      hhidden(+id) +
296  
      p("Object ID: " + htmlEncode2(id)) +
297  
      renderForm(map)
298  
      + p(hsubmit("Save changes")),
299  
      paramsPlus(formParameters(), action := baseLink + "#obj" + id));
300  
  }
301  
  
302  
  S renderPage(SS params) {
303  
    if (eqGet(params, "cmd", "new"))
304  
      ret frame("New " + itemName(), renderNewForm());
305  
    if (nempty(params.get("edit")))
306  
      ret frame("Edit " + itemName(), renderEditForm(params.get("edit")));
307  
    if (nempty(params.get("duplicate")))
308  
      ret frame("New " + itemName(), renderNewForm(data.getObject(params.get("duplicate"))));
309  
    ret frame(ahref(baseLink, firstToUpper(singleton ? data.itemName() : data.itemNamePlural())), render(mutationRights, params));
310  
  }
311  
  
312  
  HCRUD makeFrame(MakeFrame makeFrame) { super.makeFrame(makeFrame); this; }
313  
  
314  
  S cmdsKey() { ret "<!-- cmds -->"; }
315  
  S checkBoxKey() { ret "<!-- checkbox -->"; }
316  
  
317  
  S itemName() { ret data.itemName(); }
318  
  
319  
  swappable MapSO postProcessTableRow(MapSO data, MapSO rendered) { ret rendered; }
320  
  
321  
  O itemID(MapSO item) { ret mapGet(item, data.idField()); }
322  
  
323  
  swappable S renderCmds(MapSO item) {
324  
    O id = itemID(item);
325  
    ret joinNemptiesWithVBar(
326  
      !actuallyAllowEdit() ? null : ahref(editLink(id), "EDIT"),
327  
      !actuallyAllowDelete() ? null : 
328  
        !data.objectCanBeDeleted(id) ? 
329  
          span_title("Object can't be deleted, please delete references first", htmlEncode2(unicode_DEL()))
330  
          : ahrefWithConfirm(
331  
            "Really delete item " + id + "?", deleteLink(id), htmlEncode2(unicode_DEL()), title := "delete"),
332  
      !actuallyAllowCreate() ? null : ahref(duplicateLink(id), "dup", title := "duplicate")
333  
    );
334  
  }
335  
  
336  
  bool actuallyAllowCreate() {
337  
    ret !singleton && allowCreateOrDelete && allowCreate;
338  
  }
339  
  
340  
  bool actuallyAllowEdit() {
341  
    ret allowCreateOrDelete && allowEdit;
342  
  }
343  
  
344  
  bool actuallyAllowDelete() {
345  
    ret !singleton && allowCreateOrDelete;
346  
  }
347  
  
348  
  // e.g. for adding rows to edit/create form
349  
  swappable void massageFormMatrix(MapSO map, LLS matrix) {
350  
  }
351  
  
352  
  swappable S renderComboBox(S name, S value, LS entries) {
353  
    if (haveSelectizeJS) {
354  
      // coolest option - use selectize.js
355  
      S id = aGlobalID();
356  
      ret hselect_list(entries, value, +name, +id)
357  
        + hjs("$('#" + id + "').selectize" + [[
358  
          ({
359  
            searchField: 'text',
360  
            openOnFocus: true
361  
          });
362  
        ]])
363  
        // dirty CSS quick-fix (TODO: send only once)
364  
        + hcss(".selectize-input { min-width: 300px }");
365  
    }
366  
      
367  
    if (haveJQuery) {
368  
      // make searchable list if JQuery is available
369  
      // (functional, but looks quite poor on Firefox)
370  
      S id = aGlobalID();
371  
      ret tag datalist(mapToLines hoption(entries), +id)
372  
        + tag input("", +name, list := id);
373  
    }
374  
    
375  
    // standard non-searchable list
376  
    ret hselect_list(entries, value, +name);
377  
  }
378  
  
379  
  void processSortParameter(SS params) {
380  
    S sort = mapGet(params, sortParameter);
381  
    sortByField = null;
382  
    descending = false;
383  
    if (nempty(sort))
384  
      if (startsWith(sort, "-")) {
385  
        descending = true;
386  
        sortByField = substring(sort, 1);
387  
      } else
388  
        sortByField = sort;
389  
  }
390  
}

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: 444 / 1194
Version history: 139 change(s)
Referenced in: [show references]

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