Polish 'til it gleams: learning from refined, production-grade components

By Thomas Cannon

Elevator Pitch

In a step-by-step talk, I’ll walk through the entire stack of a highly polished component in Little CRM to show how to ship high-quality software with real, production code. We’ll talk about the nitty-gritty, what architectural decisions I made (and why!), and how I recovered from mistakes.

Description

Let’s learn how to ship high-quality software with real, production code. I’ll put a production component under the microscope for everyone to see. You’ll get to see the nitty-gritty technical details, the architectural decisions made (and why!), and how to dug yourself out of holes from early prototyping.

With high production value and a step-by-step walkthrough, I’ll guide you through the entire stack: stopping to pop the hood when needed, but never getting bogged down in the superfluous details. I’ll show my mistakes and the work in progress, because even seniors struggle.

By the end, you’ll have a firm grasp on the interplay between design systems, user experience, making the appropriate concessions, lightweight Javascript, using CSS to transform wholly declarative HTML, ViewComponent design, and leaning into server-side rendering. And you’ll feel empowered to apply this same level of polish in your everyday work: no “draw the rest of the owl” gaps here!

Notes

All the code from this will be available for attendees to look through after the talk.

These components, and this work, sprung out of years-long work I’ve done to level up my web development across the board. I’m calling the entire pattern library “The Practical Framework,” and I’d like to mention that during the talk so attendees have an extra place to go afterwards.

This talk was originally going to cover 2 components, but as I outlined their design and lessons for my guides, I realized that would be nigh impossible in 30 minutes. Instead, y’all can pick which one suits the conference best. I recommend the first one!

A “basic contact info” field component (name + avatar)

While this component is very frontend heavy, I still think it’s important for a Ruby conference because if we’re building for the web, we do need to use HTML, CSS, and Javascript. We can take Ruby/Rails’ focus on developer happiness and convention over configuration and apply them to the rest of the stack: having a solid foundation and example to point to for these helps make writing frontend code less painful, maybe even somewhat as joyful as writing Ruby (a tall order, I know!)

The core talking points will be:

  • Why I use Shrine for file uploads and the architecture of the client <=> server communication for cached file uploads using XHR
  • A teardown of my initial prototype’s faults
    • Cobbled together UI
    • Lack of support for mildly complex scenarios
    • Using an off-the-shelf library (Uppy) that was a bad choice for the job
  • The design goals for my new component
    • HTML: setup everything declaratively. All error messages exist on the document, making it easy to review & customize them. For dynamic elements, like the gravatar dropdown items, use a template element that we write the text into.
    • CSS: controls what’s rendered by using the cascade & selectively rendering based on the state of the Custom Element
    • JS: event delegation & dispatching (uses events to signal changes in the state). Also the Gravatar retrieval is done client-side
  • Detailed walkthroughs of version 1 (the set-avatar-element)’s:
    • ViewComponent’s markup
    • Data flow for failsafe file uploads
    • Usage of custom events
    • Refactoring to support loading Gravatars from multiple email addresses within the browser to reduce request time, and compressing the complexity of avatar processing into a single pipeline
    • Adding desktop-grade polish with drag/drop and copy/paste support, in less than 100 lines of vanilla JS! Including how to avoid bugs that ship in even the richest SPAs like Linear.
  • How I got to version 2 (a combined avatar upload + name field):
    • The impetus: reducing wasted space on the entire “Edit Person” form
    • Researching for inspiration:
      • The macOS Contacts app for the inline photo + name pattern
      • The 2013 OS X Human Interface Guideline for the “Image Well” component
    • How my use of data-* attributes and duck-typing expectations for the Custom Element’s markup allowed me to reorganize the ViewComponent to include the “Name” field without any significant changes to the CSS or JS
    • Refactoring the ViewComponent so that it’s ready for reuse when I add avatars to the Team model
  • Other implementation notes to improve the CSS and JS you write :
    • Using JS hashes to reference custom event names
    • Reducing your memory footprint by avoiding anonymous functions for event handlers
    • Remembering that javascript is a functional programming language
    • Why I use data-* attributes in some places, classes in others, and how minimal SCSS is used to control rendering while still allowing for easy debugging

A series of components for contact information (emails, phone numbers, social media handles, URLs)

This version covers data modeling pitfalls, ActiveModel forms, Rails form builders, leveraging ViewComponent slots, and migrating production data from bad decisions.

The core talking points will be:

  • A demo of the final result
  • A teardown of my initial prototype’s faults:
    • The cobbled together UI
    • The incomplete parsing logic
    • Strongly negative user feedback and lack of usage
  • An overview of the server-side modeling:
    • A Handle model, with a label and Link (upserted model for URIs)
    • A concern that can be included in ActiveModel forms for processing Handle line items, including:
      • Using a “nested” ActiveModel for the “line item”
      • Validating each line item and the list of handles generally (limiting the number of handles)
      • Saving the added handles, and removing the ones marked for removal
    • How that concern is mixed into the ActiveModel form that encapsulates the “Edit Person” form’s behavior. A standalone ActiveModel is used so that the complexities of form processing do not bleed into the underlying data models.
  • The pitfalls of my prototype’s data modeling
    • A cautionary tale of data modeling & abstraction/contextual compression. Essentially: I tried to cram everything into a single model (a Handle with a label and URI), which is beautifully simple from a data model standpoint, future-proof, and maximally compressable; but was unworkable in real-world usage.
  • Show my Balsamiq sketch for what the new UI should look like
  • Quickly outline my mitigation plan for framing:
    • Break out EmailAddress and PhoneNumber into their own ActiveRecord Model
    • Add support for standalone Links on a Person
    • Keep the Handle model, but dramatically refactor it to only cover social media handles
    • Copy the pattern of Handle processing; but for EmailAddress, PhoneNumber, and Link individually
    • Add the new view layer for EmailAddress, PhoneNumber, and Link line items
    • Write ActiveJobs to migrate the existing production data for EmailAddress, Phone, and url Handle records to the new models
  • Breaking out the EmailAddress and PhoneNumber into their own ActiveRecord Model:
    • Straightforward ActiveRecord models, with customized processing for each
  • Adding support for standalone Links on a person
    • Again, straightforward Rails code
  • Copy the pattern of Handle processing; but for EmailAddress, PhoneNumber, and Link individually
    • Show how the code is very similar between all 4 versions; emphasizing DRY for cognitive complexity, not LoC.
  • Add the new view layer for EmailAddress, PhoneNumber line items
    • Show the current Handle ViewComponents
    • Start with the EmailAddress ViewComponents, using this new ground as a “clean room” implementation
    • Refactor shared “framing” markup into another ViewComponent for reuse
    • Show how the “destroy” and “undo” markup is declarative and CSS is used to selectively render them without involving JS. Also show that it’s based on a native checkbox, with inline JS until the Invoker Commands API is Baseline.
    • Show the Custom Element that wraps each line item for auto-removing empty lines, and updating the label in the “destroy” callout.
  • Show how a standalone form is used with Mrujs to request a new line for contact info, reusing the ViewComponent and using CableCar to append the new line.
  • Quickly summarize how the same pattern is used for the Link view layer
  • Show the jobs to migrate the production data, and why I used an ActiveJob so I could easily write tests