In lesson 4 we finish building out our Minimal Todo List project, adding the ability to add new todos, create new lists with arbitrary columns, save lists to .csv files. As with the previous lesson, a major portion of the project was manipulating lists and maps with the Enum module and handling file and user IO. If you haven't seen it yet, definitely check out part 1!
(Source code available for premium members)
Building out more user commands
As explained in the previous episode, our Todo List app runs in a loop that alternately asks the user to enter a commands and executes those commands. Each top level function takes application state as a parameter called data
and each one calls get_command
, passing in a new (or possibly unchanged) application state.
add_todo: Lets user add a new todo item. We'll make this function a simple pipeline of actions as follows:
- get the name of the new todo from the user
- get the names of the column headers
-
query the user to enter a value for each field (using the header names and a
field_from_user
helper) - create a new todo from the name and field values the user has entered
-
use
Map.merge
to get a new application state that includes our new todo along with the previous data -
call
get_command
with the new data to get the user's next action
get_fields: A helper to get the column headers out of our application data. E.g. ["name", "priority", "completed"]
get_item_name: A helper used by add_todo
. It uses IO.gets
to ask the user what to name the todo, then it checks the application data to see if any todos already have that name. If the name is used, prompt the user again, if not, return the name.
prepare_csv: A helper used by save_csv
to generate a csv form of our application data, which is an Elixir Map
. It uses get_fields
to the the name of our headers, then maps over our data, using the field names as keys to extract each row. Next it joins items within each row with commas and joins the rows themselves with newlines to make one long string that represents the entire .csv. Streaming functions could be used here, but we're not worried about the scalability of our command-line todo list.
save_csv: A function to write our application data to a .csv file. It asks the user the name of the .csv file, then it calls prepare_csv
to generate a binary (i.e. "string") of the the app state and finally it uses File.write
to save the file to disk.
Adding an option when starting (29:55)
Since the application always asks for a todo upon starting, it isn't actually possible to create a new .csv except by first loading data from another one! To fix this we need to ask users upon start
if they'd like to create a new .csv. If they respond no, then we'll just use our existing logic, but if they answer yes, then we need to do a bit more work.
Minimal Todo is flexible about what fields go into todos. Users can choose whether they want todos with only "title" and "completed" fields or maybe other fields like "assigned to", "urgency", etc. Consequently, for an entirely new .csv file, we need to ask the user what fields they want their todos to have. To handle that we need a few more helper functions.
create_header: Takes a list of field titles as input, and asks the user to enter another field. If the user doesn't enter anything, the existing headers are returned. If the user does enter a new field, it recursively calls create_header
, passing in the new header prepended to the list of existing headers.
create_headers: Gives the user instructions to enter field names one by one and followed by an empty line when done, and then calls create_header
with an empty list. Returns a list of all the field names a user has chosen to input.
create_initial_todo: As with a normal add_todo
, create_initial_todo
asks the user the name of their todo and and the value of each todo field. However since it's a new todo and the no file has been loaded, it first calls create_headers
to query the user what fields will be in this (and all subsequent) todos. Returns a map containing this single todo as application state.
With these helpers in place, our start
function only needs to call create_initial_todo |> get_command()
in the case where the user chooses to create a new .csv, and load_csv()
when they choose not to.
Conclusion and Challenge 4
This is a fairly comprehensive todo app considering it's only 160 some lines of code and doesn't rely on mix or any external dependencies. Users can load their todo lists from .csv files, add or remove todos and then save them. They can also create, build and save entirely new todo lists with arbitrary field names.
The challenge this time is to do the following:
- Write an Elixir function that finds the 45th Fibonacci number
- Look at the Elixir docs for new DateTime and figure out how to time your function!
(solution here)
3 Comments
Thanks for this great tutorial. It would be nice to go over iterating via Enum.each on nested Maps like the one we're working with. I added the following to add more details to the todo list when viewing:
Nice! That's a great extension to the example.
Universal solution: