Luckily, I had a problem in need of solving. See this?
Those are packing lists. Dozens of them, and that’s only counting trips I’ve taken with my partner in the past couple of years. I’ve also made lists in Simplenote, scribbled them on the backs of envelopes or the margins of research papers, and—worst—forgotten to make lists and ended up in Toronto without a toothbrush.
It’s not a gigantic problem, but it was enough to spark Go Bag, a packing list app for people like me.
Quick links, in case you want to skip the chatter below and go straight to the code:
During this project, I experimented with keeping a “dev log”—quick notes on what I did each day. It was a good way to track my progress and make note of anything that stumped me/new things I discovered. I kept it in Simplenote, and kept my running to-do list at the bottom, which made it easy to move tasks up into the log one at a time as I worked on them and gave me a space to offload ideas for the future and things I didn’t want to forget (like “double check that that button still works” or “write documentation”). A++ would do again.
I started by sketching out a data model in LucidChart:
We were given a template for a Rails API that included a users resource and authentication. Starting there, I used Rails’ scaffolding to generate models / migrations / routes / controllers / serializers for my lists, items, and contents, then set up the relationships among everything.
On 9/26 and 9/27 we learned about automated testing using RSpec. It was the best. I decided to try to backfill in tests, but I got tripped up with authentication and, in the interest of time, decided to move on.
This was our first official project day. I spent the day:
- Writing and testing curl scripts for every action I wanted to perform with my API.
- Protecting lists, items, and contents by having their controller classes inherit from the provided ProtectedController class, which checks to make sure the user is authenticated before providing access to the resources.
current_userto make sure users can only access their own packing lists.
- Sketching wireframes.
- Building out forms and API calls from the front end for authentication, based heavily on my Tic Tac Toe app. I was pretty excited to be able to refactor my log in function(s)— I was previously using two separate log in functions, one to handle “regular” logging in, and one to handle automatic log ins for users who had just signed up. I was able to condense this into a single function, which felt great.
- Starting to separate HTML into Handlebars templates. For this project, we were given the same client-side template as we were for the Tic Tac Toe games. One of the more confusing/frustrating pieces of that project was figuring out how and when to hide/show different DOM elements, and how to make sure that what was displaying on the page always matched up with what was on the server. My game appends things on the page and then sends data to the server, which works functionally but also a) is a lot of steps, and b) leaves open the potential for the client and the server to get out of sync. Working with Handlebars made the concept of “re-rendering” click for me—I can grab data, feed in into a template, and replace everything currently on the page or in a specific section with the product of that template. Figuring out how to split things up and making sure I had the appropriate elements in the DOM to target for replacement/re-filling was a bit tricky, but once I got the hang of it, I felt like I could work more smoothly and cleanly.
Kept working with Handlebars.
Signed up for a Heroku account and deployed my API. It took me a while to figure out that my global
~/.gitignore file had been set (as part of GA’s Installfest) to ignore all
secrets.yml files, which hold the environment variables for secret keys. (I had even checked
~/.gitignore_global, which is the sample file name GitHub uses when talking about global gitignore files, but hadn’t looked inside of
~/.gitignore. Oops.) This caused issues trying to set secret keys on Heroku, and a friend and I spent a significant chunk of the morning working through this. I ended up forcibly adding the file to my repo, which felt scary and bad (but worked!), before one of the instructors jumped in and explained the
After that, I went back to Handlebars and discovered the hard way that you can’t register event handlers to DOM elements that aren’t on the page when it initially loads. Whoops! I went back and fixed that by registering events on elements that *do* exist, then moved on to building functionality that allows authenticated users to create lists and add items to a list.
One of the pieces of flair I really wanted for this app was the ability to type a potential packing list item—say, a toothbrush—into an input field and have an autocompleted list of potential items to choose from pop up. This felt like smoother UX than checking off items from a super long list on a page, and it tied in with my desire to avoid having a different toothbrush in the database for each user.
I decided to use the Devbridge jQuery-autocomplete library for this. I spent the rest of the day getting this to work, with a couple of challenges/steps:
- I needed to implement search functionality in my API for autocomplete to work.
- Despite the fact that the library claims to allow you to pass in headers, I saw multiple issues filed on the repo from people who couldn’t successfully pass an auth token through in their API call. I couldn’t get this to work either, so I ended up unprotected the index/show methods for my items controller in order to get autocomplete to work.
- The library wants your potential autocomplete suggestion data to be formatted in a very specific way, so I needed to write a function to transform the data coming back from my API.
Towards the end of the day, I had a fully functional autocompleting input field. Typing in “to” would offer “toothbrush” and “toothpaste” and “Tootsie rolls” (assuming all three of these existed in the
items table already) as possible options. Success! From my perspective, this is the coolest part of my project. It feels polished in a way that not much else I’ve built yet does, and it adds functionality—it helps suggest things to users that they might forget and offers a tiny bit of serendipity (typing in “s” to get to “sweater” will give you a list of options that includes “swimsuit,” which might help you make use of the hotel pool in Boston in February). It also, I think, looks nice.
Restraining myself from using hyperbole and exclamation points in my commit messages but in my heart they mostly read “DID MAGIC!!!!!"
— RebekahHeacockJones (@rebekahredux) October 1, 2016
I was feeling pretty impressed with myself, but that balloon popped almost immediately when I realized I hadn’t thought carefully enough through the UX workflow to know how to build the next piece, where choosing an item from that pool of suggestions lets a user add it to a list, and typing in something that’s not in the pool of suggestions creates a new item and then adds it to the list. I spent a few minutes hashing this out, referencing Avocado (a list-making app for pairs of people) heavily in the process. At the end, I had this:
- form will have a hidden input element for the list id
- enter an item, save
- after saving, display another item input field so the user can add another item
- focus first on items that exist in db
- figure out later how to add a new item using same UI (ultimately decided: if item exists already, save a “contents” association between the list and the item. If it doesn’t, first save the item, then save the contents association)
That gave me enough to work on starting on Saturday morning.
I got single item addition working, then moved on to letting users add multiple items in succession. This involved redrawing the list after saving the item, which meant going back to my serializers to make sure I was passing sufficient data through in order to be able to access a list’s title and id plus all of its items’ ids and names. I also added some validation to the Contents model so that a list can’t contain more than one of the same item.
After this, I realized that I hadn’t done enough testing after splitting up my HTML into different templates, and I had broken more of my click handlers than I realized, particularly around authentication. Bug fixing time!
Since I was working with authentication anyway, I decided to build user profile functionality, where logging in gets a user’s email and lists and renders the user’s list titles (including links that let the user edit each list individually) on the page. This gave me some code I could reuse when switching to and from different views/states while authenticated, which was an added bonus.
I fixed some more click handlers, played around with the default order of lists and items coming back from Rails, and fixed a bug that was popping up when a user tried to add an item (say, “toothbrush holder”) that starts with the name of another item (“toothbrush”). This turned out to be fairly simply, and involved using an option that comes with the jQuery-autocomplete library that is activated when you “invalidate” input (in other words, when you keep typing after already filling the input with a valid option from the list of suggestions).
I started Sunday with lots of deleting: items from lists, and lists from a user’s account. I decided not to offer item delete functionality through the client—I didn’t set up my data model in such a way that the items a user adds are associated only with their accounts, and I didn’t want users to delete items that would then be (surprise!) removed from other users’ lists. This was a deliberate decision, as I wanted the things a user adds to their list that I haven’t, as the maker of the app, already thought of to be available to other users. For example: if I seed the items table with “insect repellant” but someone else adds “bug spray” to their list, I want other users to be able to find and add “bug spray” as well. There seemed to me to be enough value in that collectively generated set of items that I didn’t want to hide it from users.
There are cons to this approach that I haven’t quite figured out yet. The first is privacy. If you add “Helga’s Wellbutrin” or “surprise birthday present for Alistair,” those items are available for everyone to see, which is a bad thing. The second relates to usefulness: as Fran, I don’t care about or want my options cluttered with Helga and Alistair’s stuff. When I was presenting Go Bag, I talked a bit about this, and I think my next step is to enable a seeded list of items available to all users, but confine new items to specific users’ accounts. Each user will be able to see the seed list plus the items they’ve added, but not the items anyone else has added. Another step past this would be to let users suggest new items for the seed list, and create an admin account with the power to approve/deny these requests. “Bug spray” would get added, but “lolBUTTS” would be visible only to email@example.com.
After building the client-side deletion features, I added a checkbox to mark an item within a list as “packed” and updated the API to send back items in a list ordered first by unpacked vs packed, and then, within those groups, with the most recently updated items first. Given that I’m re-rendering the contents of a list each time they change, this means that clicking the checkbox next to an item to pack it will move it toward the bottom of the list, to the top of the section of packed items. Unchecking it will move it back to the top of the list. Since I’m getting all list items (“contents” in my data model) through the lists serializer, I realized I don’t need dedicated index or show methods for contents, so I removed those from the ContentsController class.
At this point, I decided to get what I had up on GitHub pages and make sure it worked with my deployed API. After I set the
CLIENT_ORIGIN on my deployed API to my GitHub pages URL, everything worked as expected.
I moved back to the client side of things and moved error messages out of
console.log() statements and into the UI. I also added some lightweight validation on lists, so that a user can’t have two lists with the same title.
At this point, I felt like I had all of the client-side functionality that I absolutely needed in order to submit a working project. This freed me up to start working on design, which I both like (shiny things!) and find trying: inventing a design from scratch while simultaneously writing HTML/CSS feels a bit like building the airplane while it’s already in the air. I could solve this problem by putting together better, full-fledged mock-ups with art and typography, but a) that’s not practical on the timeline we have for GA projects, and b) I’m still mourning my loss of access to the Adobe Creative Suite, and I haven’t yet bothered to acquire/teach myself replacement software.
I spent the next few hours working with Handlebars templates, SCSS, stock photography, and Google fonts. I took a few small breaks to build tiny bits of actual functionality: for example, ensuring that clicking on the app’s logo in the upper left would load the default home page for visitors but the correct user’s profile for authenticated users.
Once I had the home page and authentication forms looking mostly how I wanted them to look, I gave myself a reward and built a list title editing feature (in case your New Hampshire camping trip gets rained out and you end up going to the Catskills instead) and—this was super cool—the ability to clone lists. I find myself duplicating my lists in Google docs all the time so that I can tweak an existing list for a new trip instead of starting with a blank slate. In Rails, I used the deep_cloneable gem to clone a record (list) with its associations (contents/items). I didn’t have to write much code for this, but it felt like absolute magic. A couple of tips:
- Cloning uses a POST request, which expects data. All the data you need is already in the database (which is why you’re cloning in the first place), so you can send an empty data object along with your request to fix the
HTTP/1.1 411 Length Requirederror.
- I had set up validation so that lists can’t have the same title. To make sure the cloned record validates, I had to edit the title before saving—I prepended “copy of” so that it would be clear to users what had happened.
At this point, it was Sunday evening, and rather than jumping into building new stuff, I decided to work on documentation for my API. I had Willow’s tweet in my head while I was typing:
Be the adult you wish you had around when you were a child.
Write the documentation you wish you had when you started on this project.
— Will-o-the-Wisp (@willowbl00) August 16, 2016
I know not everyone gets as excited as I do about rules and guidelines, but to me, this is a thing of beauty.
On Monday, I went back to styling. Such SCSS. So typing. Cool things I learned:
- You can use
word-wrap: break-word;to break up a really long string of characters (say, an email address like firstname.lastname@example.org?) so it doesn’t, for example, run off the screen on mobile.
- It’s frustrating to work with Handlebars templates + CSS + JS. Moving things around in Handlebars will (inevitably, again) break your functionality, and relying on the same classes for styling and functionality is starting to feel more and more precarious. I’m curious about best practices here—I know at least one company that has separate classes for CSS-related things and for JS-related things, which makes your markup a bit longer/clunkier but sounds kind of attractive at this point.
I grumble about this, but I’m pretty happy with the way the UI turned out:
I spent the rest of the day writing up documentation for the front-end repo (a shorter version of this, plus Agile user stories, wireframes, and a list of dependencies), putting together a seed list of items, and tweaking my API documentation.
Last project day! I spend the morning cleaning up (taking out
console.log() statements; organizing and refactoring to the best of my ability) and adding a few extra touches: a favicon, a “loading” icon that replaces the “Sign Up” and “Log In” button text while the form is processing, and adding smooth internal scrolling when you click the “learn more” link on the home page.
I sent the app to a few friends and family for “beta testing,” and my sister discovered a bug when clicking the home button immediately after adding a new list—I needed to re-fetch the user profile data before re-rendering the profile view. (Thanks, Katie!)
At this point, I had half a day left, so I decided to venture back into RSpec. I started by reading through a bunch of articles on testing Rails APIs with Rspec. Many of them recommended using two additional tools, both from Thoughtbot: Shoulda Matchers and Factory Girl.
I was writing tests for one of my models, and I already had set up / tear down steps written to create and delete instances of that model for testing, so I decided to skip Factory Girl for now and experiment with Shoulda.
Readers, I liked it.
— RebekahHeacockJones (@rebekahredux) October 4, 2016
After this, I ran into an issue with authentication inside of a test that I still haven’t figured out (hoping to work one-on-one with an instructor soon, as even after some back-and-forth and trying out multiple methods of handling authentication tokens within the test, I’m still getting an “HTTP Token: Access denied” error). It didn’t put me off of automated testing, though—I’m determined to get this working. The first “real” CS course I ever took was CS50, and our first problem set included tests for the C programs we were writing. Seeing those green smiley faces was SO. COOL. I want my code to do that—to not only work the way I expect it to work, but to self-verify that it works the way I expect it to work, and to do that in a way that communicates clearly to other people who work on the same code what should happen and what, if anything, is broken.
Presentation day! Once again, the random presentation order put me at the end, which gave me all morning and all of lunch to be nervous.
I used the same tactic I used last time, of pulling up a bunch of tabs with things I wanted to make sure I talked about. I overdid it a bit because I wasn’t sure how long each thing would take, so I only made it through half of the things on my list before the timer beeped, and I felt like I was rushing things and not actually making the points I wanted to make. Overall, though, it went okay—I got some good questions about my approach to testing (all the curl scripts, plus some general sadness about not working with RSpec as much as I wanted to) and—I was kind of surprised by this!—people seemed really interested in how I implemented the random travel quote that’s displayed at the top of the page when you log in. (It’s an array of strings; I pick a random one and send it to the Handlebars template each time the profile is rendered.)
Two projects down, and two to go!