7
$\begingroup$

I'd like to see an example of a basic create-read-edit-delete application using the wolfram cloud.

For this a to-do list app would be best - where you can add a Todo item with details, and edit and remove them as well.

  • There are many ways to displaying dynamic data but all that I've tried are inefficient (takes too long to load)
  • It's unclear how to design a UI for editable data with things like FormPage and FormFunction
$\endgroup$
6
  • $\begingroup$ This is far too broad. Please include an explanation of the functionality you seek and sample code. $\endgroup$
    – MarcoB
    Sep 21, 2022 at 13:59
  • 3
    $\begingroup$ This is a great question. The powers and limitations of the wolfram clouds functions are not at all clear from the documentation on them. $\endgroup$
    – Diffycue
    Sep 21, 2022 at 23:49
  • $\begingroup$ Great question! It's insane that basic crud is not a featured example in wolfram cloud functionalities. $\endgroup$
    – M.R.
    Sep 22, 2022 at 22:38
  • $\begingroup$ I doubt whether this is not a duplicate question to, for example, tableview interactive use, TableView Dynamic or how to operate on a dynamic dataset. Problem seems to my the idea of dynamic in the question. $\endgroup$ Sep 24, 2022 at 16:52
  • $\begingroup$ This is not a dupe, because it asks how to use the cloud form apis $\endgroup$
    – user5601
    Sep 25, 2022 at 15:40

3 Answers 3

3
+100
$\begingroup$

Pardon my lack of SE-fu. I'm not sure how to publish something as a notebook, and I'm not sure what the consequences are if I publish this in my own cloud space and provide a link. So, it's just raw code in text.

Initialization

(*Initialize a cloud object to hold data.*)
ToDoList = CloudObject["ToDoList"];
CloudPut[{}, ToDoList];

To-do representation

(*I'll represent a to-do item as a structure with head ToDo. I'll
create some convenience constructors. The basic structure has a
description, a creation date, and a due date. I ended up never using
the creation date, but I had a variety of possible uses in mind.*)
NewToDo[description_String] := NewToDo[description, 1];
NewToDo[description_String, dueInDays_?NumberQ] := 
  NewToDo[description, DatePlus[Now, dueInDays]];

(*Why work with associations? Well, that's the form generated by 
FormFunction, which I eventually used to create the web form.*)
NewToDo[data_Association] := 
  NewToDo[Lookup[data, "Description", "Unspecified to-do"], Lookup[data, "DueDays", 0]];
NewToDo[description_String, dueDate_DateObject] := 
  ToDo[description, Now, dueDate];

(*Some accessors to keep things clean later on.*)
DueDate[todo_ToDo] := todo[[3]];
Description[todo_ToDo] := todo[[1]];
DisplayToDo[todo_ToDo] := 
Description[todo] <> " (" <> DateString[DueDate[todo], {"DateTimeShort"}] <> ")";

Forms for creating the to-do items

(*A simplistic method for persisting a list of to-do items.*)
SaveToDos[todoRepo_CloudObject][todos_List] := 
  CloudPut[Join[CloudGet[todoRepo], todos], todoRepo]

(*The form object and form function for creating a to-do item.*)
ToDoCreator = 
  CompoundElement[<|"Description" -> "String", {"DueDays", "Days until due"} -> "Number"|>];
ToDoListCreator := 
  FormFunction[
    {"ToDoList", "To do list"} -> RepeatingElement[ToDoCreator], 
    SaveToDos[CloudObject["ToDoList"]][Map[NewToDo, #ToDoList]] &];
CloudDeploy[ToDoListCreator, "ToDoListCreator"]

Forms for completing the to-do items

(*A simplisit method that can filter out items to be deleted (marked 
as complete) based on the string description.*)
MarkComplete[todoRepo_CloudObject][descriptions : {__String}] := 
  CloudPut[
    Select[CloudGet[todoRepo], FreeQ[Alternatives @@ descriptions]], 
    todoRepo];

(*Form object for displaying a to-do item, and form function for 
handling the (boolean) results from submitting the form.*)
ToDoCompleterSpec[todo_ToDo] := <|{Description[todo], DisplayToDo[todo]} -> "Boolean"|>;
ToDoListWorker := 
  FormFunction[
    Join @@ ToDoCompleterSpec /@ CloudGet[CloudObject["ToDoList"]], 
    MarkComplete[CloudObject["ToDoList"]]@*Keys@*DeleteCases[False]];
CloudDeploy[Delayed[ToDoListWorker], "ToDoListWorker"]

Now, if you visit the urls associated with ToDoListWorker and ToDoListCreator, you can achieve some basic functionality. If you want some sort of "all-in-one" app, you can design the forms with navigation between the edit/complete screens, or use compound form functions. I hesitate to go further without knowing if the success criteria are going to be overly complicated (or if we're going to get into a moving-the-goalpost situation).

$\endgroup$
5
  • $\begingroup$ This is cool. 1) How would you allow editing of the descriptions? 2) Is there a way to redirect after pressing submit to go back to the "home page" that displays the current list? 3) How would you allow adding attachments e.g. images per task? $\endgroup$
    – user5601
    Oct 6, 2022 at 3:22
  • $\begingroup$ If I get time in the next couple of days, I'll try to update the answer, but rather than leave you hanging, here are some thoughts. (1) probably make the complete-to-do page use an input field for the description instead of just making it a label. And you'll need to update the handle function for the form. And thinking ahead, might want to mark to-do items as complete by setting a complete date rather than just removing them. That way everything is just one big edit. (2) Pretty sure you can do this with a multi-page FormFunction, but this needs to be confirmed. $\endgroup$
    – lericr
    Oct 6, 2022 at 4:30
  • $\begingroup$ (3) Adding images to the ToDo representation is easy, as is displaying them on the page. Not sure what controls to use for adding/editing them. $\endgroup$
    – lericr
    Oct 6, 2022 at 4:30
  • $\begingroup$ (3 continued) Do you want to fetch the images from your local computer, from another wolfram cloud resource, or some other remote web resource? (And although this is interesting, manipulating images wasn't a requirement that could have been assumed based on the description of creating a to-do app.) $\endgroup$
    – lericr
    Oct 6, 2022 at 4:55
  • 1
    $\begingroup$ No worries about images. I keep getting this “do u want to do resubmit” warning. I think the major drawback is that we can’t create/ edit/view/delete from a single page. Is that possible? $\endgroup$
    – user5601
    Oct 6, 2022 at 13:08
2
$\begingroup$

Okay, here's an update that addresses some of the comments and demonstrates some more sophisticated behavior.

This has only been lightly tested. Also, I did no work on styling the UI. I don't know what was causing those resubmit errors before--I haven't seen anything like that with either of these versions.

(*These are just descriptions of the representations I'll use. 
Deleted ToDo items will be removed from the repository and thus will 
have no representation (you could choose to add something with a 
deleted at property).*)
Protect[ActiveToDo, CompleteToDo];
ActiveToDo[id, description, createdAt, dueAt, recurrenceFn];
CompleteToDo[id, description, createdAt, dueAt, completedAt];

(*Accessors*)
Id[todo : (_ActiveToDo | _CompleteToDo)] := todo[[1]];
Description[todo : (_ActiveToDo | _CompleteToDo)] := todo[[2]];
CreatedAt[todo : (_ActiveToDo | _CompleteToDo)] := todo[[3]];
DueAt[todo : (_ActiveToDo | _CompleteToDo)] := todo[[4]];
RecurrenceFn[todo_ActiveToDo] := todo[[5]];
CompletedAt[todo_CompleteToDo] := todo[[5]];

(*Display functions*)
DisplayToDo[todo_ActiveToDo] := 
  Description[todo] <> " (" <> DateString[DueAt[todo], {"DateTimeShort"}] <> ")";
DisplayToDo[todo_CompleteToDo] := 
  Style[Description[todo] <> " (" <> DateString[DueAt[todo], {"DateTimeShort"}] <> ")", FontVariations -> {"StrikeThrough" -> True}];

(*Transformations*)
(*I've added the idea of recurrence, so completing a recurring task 
results in a new active task along with the original task transformed 
to completed. I've provided some sample recurrence functions.*)
Complete[todo_CompleteToDo] := {todo};
Complete[todo_ActiveToDo] := 
  {ReplacePart[CompleteToDo @@ todo, 5 -> Now], 
   RecurrenceFn[todo][todo]};
RecurDaily[todo_ActiveToDo] :=
  NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, 1], "Recurrence" -> RecurDaily|>];
RecurWeekly[todo_ActiveToDo] :=
  NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, 7], "Recurrence" -> RecurWeekly|>];
RecurMonthly[todo_ActiveToDo] :=
  NewToDo[<|"Description" -> Description[todo], "DueAt" -> DatePlus[Now, Quantity[1, "Months"]], "Recurrence" -> RecurMonthly|>];
RecurNever[_] := Nothing;


(*Constructors*)
(*I didn't bother with constructors that don't use an association 
argument, since this is intended to be used with a form.*)
NewToDo[] := NewToDo[<||>];
NewToDo[data_Association] :=
  With[
    {id = CreateUUID[]},
    ActiveToDo[
      id,
      Lookup[data, "Description", id],
      Now,
      Lookup[data, "DueAt", DatePlus[Now, Lookup[data, "DueDays", 1]]],
      Lookup[data, "Recurrence", RecurNever]]];

(*Initialize a cloud object and give it some data.*)
ToDoList = CloudObject["ToDoList"];
CloudPut[{}, ToDoList];(*use this for true initialization, the following was for testing*)
initData = 
  {NewToDo[<|"Description" -> "take out garbage", "Recurrence" -> RecurWeekly|>], 
   NewToDo[<|"Description" -> "clean filter", "Recurrence" -> RecurMonthly|>], 
   NewToDo[<|"Description" -> "one time task", "Recurrence" -> RecurNever|>]};
CloudPut[initData, ToDoList];

(*The following map allows us to provide a friendly UI for our 
recurrence functions.*)
RecurrenceMap = {"None" -> RecurNever, "Daily" -> RecurDaily, "Weekly" -> RecurWeekly, "Monthly" -> RecurMonthly};

(*Simple creator widget.*)
ToDoCreator = 
  CompoundElement[
    <|"Description" -> "String", 
      {"DueDays", "Days until due"} -> "Number", 
      "Recurrence" -> Keys[RecurrenceMap]|>];

(*The widget for existing tasks is a bit more complicated. Basically, 
we'll associate the task id with a pair of booleans that allow you to 
either complete or delete the task.*)
ToDoCompleterSpec[todo_ActiveToDo] := 
  {{Id[todo], DisplayToDo[todo]} -> CompoundElement[{"Complete?" -> "Boolean", "Delete?" -> "Boolean"}]};

(*This function is kind of messy, but suffices for a demo. The form 
will give us a bunch of old keys with the complete/delete choices, 
and it'll also have one item that contains a list of all of the new 
tasks. We gather all of these items into groups and process them 
against the existing tasks. The HTTPRedirect at the end allows us to 
return to the same page, making it sort of like a self-contained 
app.*)
HandleThenReturn[data_] :=
  With[
    {oldData = CloudGet[CloudObject["ToDoList"]],
     newItems = NewToDo /@ Lookup[data, "NewToDoList", {}] /. RecurrenceMap,
     completeIds = Keys[Select[data, #["Complete?"] &]],
     deleteIds = Keys[Select[data, #["Delete?"] &]]},
    CloudPut[
      Flatten[
        {newItems, 
         Map[
           If[MemberQ[deleteIds, Id[#]], Nothing, If[MemberQ[completeIds, Id[#]], Complete[#], #]] &, 
           oldData]}], 
      ToDoList];
    HTTPRedirect[CloudObject["ToDoListWorker"]]];

(*And here is the form.*)
ToDoListWorker := 
  FormFunction[
    Flatten[
      {ToDoCompleterSpec /@ Cases[CloudGet[CloudObject["ToDoList"]], _ActiveToDo], 
       Delimiter, {"NewToDoList", "New Tasks"} -> RepeatingElement[ToDoCreator, {0, {0, Infinity}}]}], 
    HandleThenReturn];

(*Deploy the form.*)
CloudDeploy[Delayed[ToDoListWorker], "ToDoListWorker"]
$\endgroup$
5
  • $\begingroup$ This is nicer! 1) Is this what the ToDo list (not worker) should look like: imgur.com/duTcAem 2) It seems the complete checkmark doesn't do anything visibly different from delete, is that as designed? $\endgroup$
    – user5601
    Oct 11, 2022 at 0:57
  • $\begingroup$ 3) is there a way to show completed in gray or make the descriptions editable in place? $\endgroup$
    – user5601
    Oct 11, 2022 at 0:58
  • 1
    $\begingroup$ If you check the delete box and submit, that particular to-do will no longer be in the list of all to-do items. If you check the complete box and submit, a CompleteToDo will be created. The idea was that you might want a history of completed tasks. $\endgroup$
    – lericr
    Oct 11, 2022 at 16:10
  • $\begingroup$ As for the "editable in place", I don't know. As far as I can tell, FormFunction and related functions all expect you to actually submit the form for any action to take place. That's the way I designed this little demo. You check complete or delete as desired and then click submit. You're brought back to the same page, so it has somewhat of an interactive feel, but yeah, not quite "in place". $\endgroup$
    – lericr
    Oct 11, 2022 at 16:13
  • $\begingroup$ I've just never tried "editable in place" before, so I don't have any confident answers. Maybe you turn the checkboxes into buttons. $\endgroup$
    – lericr
    Oct 11, 2022 at 16:16
1
$\begingroup$

Correct me if I am wrong but I think there are two options.

  1. A Cloud Noteboook with a Dynamic based interface which encapsulates GUI, communication with 'back end' and interface to the storage. The storage itself can be in form of CloudExpressions or PersistentValues or simple files.

    That's fine for prototypes but responsiveness of a Dynamic heavy notebook is far from optimal. It is hard enough to make complex apps for desktop FE.

  2. A Javascript app bundled with REST-like APIFunctions associated a storage (see above).

    Here is an example of a JS app in WPC backed with and API function. It does not include persisted storage but you get the point https://mathematica.stackexchange.com/a/173787/5478 (This is an old topic so the code might not work anymore.)

    Keep in mind that CURD methods can't be implemented beased on HTTP methods because the Cloud does not support PUT and DELETE methods. So you will need to have specific endpoints /delete-something or a generic api function which will dispatch a method based on the request body e.g. {type: delete, task: `id`}

    And if you really need WL capabilities to show something which can't be exported as static SVG or whatever then you can embed exactly and only what you need as a small notebook with a Wolfram Notebook Embedder

And for anything serious I'd use the second option.

Why not FormFunctions/Pages? Because that's not their purpose and the interactivity of the form is pretty limited anyway. Do you want to submit every C-R-U-D action via the same Submit button? And what then, should it redirect to itself with updated data?

$\endgroup$
4
  • $\begingroup$ But what about a self contained wolfram app? Do we really have to use react to get this basic functionality? If so, then what's the point of using the wolfram cloud at all? $\endgroup$
    – user5601
    Oct 5, 2022 at 22:25
  • 1
    $\begingroup$ @user5601 I forgot to mention that the storage could be a DynamicModule variable but still, the notebook interface does not scale with interactivity and depth. $\endgroup$
    – Kuba
    Oct 5, 2022 at 22:43
  • $\begingroup$ Is there any good example of option 2 in github? $\endgroup$
    – user5601
    Oct 5, 2022 at 23:17
  • $\begingroup$ @user5601 Not that I am aware of but in principle the UI is just an any webpage consuming api provided by APIFunctions $\endgroup$
    – Kuba
    Oct 7, 2022 at 11:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.