A Super Calendar

Jordan Morano
Icarus’ Wings
Published in
4 min readMar 1, 2017

Recently, we finished work on a major feature upgrade for one of our clients, Fitbot, a web application for professional personal trainers. We were tasked with making a custom calendar and workout editor that had a lot of crucial features:

  • Inline editing and inserting
  • Drag and drop
  • Bulk editing and deleting
  • Popovers and modals

While building out the calendar to meet the needs above, we quickly ran into the issue of performance. A trainer would use the calendar to update a specific client’s workouts, which could be a lot of workouts if they were a long-term client of the trainer. We also developed a nested tree of components that would handle all of the inline editing that the calendar required (and to make the feature worthy of the upgrade), but it was really painful if we had to re-render any of these component trees at all since each one had so many child components.

Optimize request to the server

Scope request to the server to just workouts that we needed

We recognized that the tool that we were building would be used heavily by the core users of the app platform. Therefore, we knew off the bat that we wanted to scope the requests to the server to just the data that the trainer would need. We handled this by requesting workouts for the previous, current, and next months. When a trainer would change months, we would fetch the workouts that were needed in order to always maintain the one-month buffer around the current month. We separated the request for the current month from the buffer months, allowing for the latter requests to not have any negative impact on the page load time.

Cache workouts and track ones already requested

We also added a simple caching system that would track the months for which workouts were requested and not make duplicative requests when shuffling between months. The calendar route’s model pointed to the cache, allowing it to immediately resolve if the cache already contained the required workout data.

We went with storing the fetching and caching logic on the parent model, the client, which also enabled us to easily test it.

Manually handling workouts on each day

Creating a workout distributor service

The calendar had some robust requirements and to meet all of them, we used a nested component structure, e.g. workout-editor/workout-calendar and workout-editor/workout-form. One of the features was for a trainer to easily update the day for a single workout or a group of them. Updating the day of a workout impacted where the workout would appear on the calendar. Our initial fallback approach was to use computed properties and re-cache the day’s workouts when the due date changed on them. This had a huge cost with re-rendering, especially since we had such a complex tree of required components. Instead, we managed the collection of workouts on a day manually through a service that allowed us to scope the changes to only the days that were impacted vs globally clobbering and resetting all the day workouts.

The service was in charge of all rendering of workouts on the calendar from the initial load to bulk changes, shuffling around workout days. When the trainer would delete a group of workouts, we would have the service update and remove the workouts from each calendar day. It also handled more complex scenarios of inserting a group of workouts, starting on a single day and retaining the same spacing, e.g. Workout — Day 1 and Workout — Day 5 have a spacing of 4. We needed this in order to paste copied workouts or when dragging a group of workouts. Besides updating the day collections, the service also had to handle updating and persisting the information to the server, updating the workout due date and order (the position of the workout in the day).

Multiple calendar types

The above also had to be handled across not just one but two calendar types with subtle differences. A client calendar would have workouts with due dates, while a program calendar would have workouts just with positions and no due dates. The position or due date would help us determine the correct spread when applying bulk changes to the workouts, which is pretty straightforward on the surface. Rather than add complexity to the service, we offloaded some of it to a Day model with a subclass specifically tailored for a client calendar and another for a program calendar.

All put together

To recap, the calendar route’s model would grab the cache data and make a server request if required. The service then would grab a copied reference to that model collection and take over managing where the data would appear on the calendar. Each calendar day was a model with logic that would help the service distribute or redistribute the workouts when called upon to do so. The Day model’s logic was especially useful when we needed to extend it with adding the ability to copy workouts across calendar types while maintaining the proper spacing.

In the end, managing the collections manually was riskier but provided a huge win with page rendering and making the entire experience of moving a group of workouts feel fast and fluid. Also, having all of the important logic in the service and model made it really easy to test. We were able to build a robust calendar that meets all of the many needs of its users.

Acknowledgements and appreciations

A big thank you to Fitbot for giving us the opportunity to spearhead development on this calendar feature. We hope that its core users enjoy the new interface and appreciate the efforts we made in order to boost their productivity.

The ‘we’ referred to several times in this post is me, Jordan, and my business partner, Patrick Berkeley, at our Ember + Rails/Elixir consultancy, IcarusWorks. As we indicated on our website, we love solving interesting problems with the latest web technology tools. We’re a scrappy and small team that can do all of the above — software strategy, architecture and development. We love hearing about new problems and/or products and seeing if we can work together. Please feel free to reach out!

--

--