How We Write Code

This is a document originally intended for internal use, which outlines all the patterns we use internally at Really Good Software.

Pre Reading

Patterns

Form Inputs

Text Inputs

Normal text input elements inside a form with the ui-form class - base styles will take care of styling.

<form class="ui-form"> 
  <input type="text">
</form>

Text Inputs with floating labels

Base Styles lets us have labels that appear as placeholders when a text input is focussed.

<form class="ui-form">
  <div class="ui-floating-input">
    <input type="text" id="floating_example" placeholder=" ">
    <label for="floating_example">First Name</label>
  </div>
</form>

*Phone Number Inputs

We’re currently using a mini js component for this but I’m not super happy and would like to improve it. It can be found in the Navan codebase and the AddOne codebase.

Simple Selects

When your select only has a few options and doesn’t require searching or selecting multiple options, a normal select element in a form with the ui-form class.

<form class="ui-form"> 
  <select>
   <option ...></option>
  </select>
</form>

Custom Selects

If we need to have searchable options, multiple options, or loading data in remotely, we can use the Select.js preact component.

  • Read More:

File Uploads

Most of the time we default to using Dropzone instead of the native file upload. Read the article here on how to set this up correctly.

<form>
  <div class="dropzone"></div>
</form>

Simple Date Pickers

For simple date pickers we can use the native browser date picker. Add the onfocus attribute for better UX to trigger it when clicked.

<form class="ui-form">
  <input type="datetime-local" onclick="this.focus()" />
</form>

*Custom Date Pickers

I haven’t fully explored the options here, but we’ve used Flatpickr in Miguel’s Navan project which seemed to work well. I would love to build a version of <better-date-picker> also which builds on the work Caleb Porzio is doing with his flux Date Picker

Loading States

We use HTMX’s built-in loading indicator feature to show loading spinners. To do this, we add a class of .shown-while-loading to the loading icon.

On Forms

<%= form_with model: @user, url: user_path(@user.id), html: {class:"ui-form"} do |form| %>
  <%= form.button class: "ui-button --solid" do |button| %>
    Save  
    <%= inline_svg_tag("heroicons/check.svg") %>
    <%= inline_svg_tag("misc/spinner.svg",class:"shown-while-loading") %>
  <% end %>
<% end %>

On Buttons

Forms work out of the box, but for links you have to tell htmx which element to apply the behaviour to, with hx-indicator=this.

<a href="/link" class="ui-button --solid" hx-indicator="this">
  New Category
  <%= inline_svg_tag("heroicons/plus.svg") %>
  <%= inline_svg_tag("misc/spinner.svg",class:"shown-while-loading") %>
</a>
Other Patterns

Modals

Modal styling is powered by Base Styles, and functionality is powered by H1 Rails. Implementing a modal is one line of code in the controller. Documentation here.

Toasts

Toast styling is powered by Base Styles, and functionality is powered by H1 Rails. Creating toasts is one line of code in the controller. Documentation here.

Simple Tooltips

We use Base Styles tooltips for this.

Rich Tooltips

Mini js works very well for this, because we are just toggling a class on an element on mouseenter/mouseleave.

TODO: Finish this example
<div :mouseenter="showPopover=true" class="relative">
   <div class="absolute bottom-0" :class="showPopover ? '' : 'hidden'" >
   
   </div>
</div>

*Sortable Lists

We don’t currently have a go to for sortable lists.

The go-to for this is htmx using a form with hx-trigger="input changed from:#el"

<form hx-get="/search" 
      hx-trigger="input changed delay:500ms from:#search-input" 
      hx-target="#results">
    
    <input type="text" 
           id="search-input" 
           name="q" 
           placeholder="Search...">
           
    <div id="results">
    </div>
</form>

Dependent Selects

This is when changing one dropdown updates the options in another one. This is generally done a case by case basis. HTMX has a pattern which they outline here. We’ve also done another pattern using Mini Js on the Hyperfly home page.

Quick Reference

Links

• Use <a href="<%= my_path %>">Click Me</a>. Avoid using link_to • HTMX will be applied by default, so no need to add any hx- attributes by default. • To remove/reset htmx on an individual link, do <a hx-boost="false" ...></a> • Use the url helpers current_url_with , current_params_with if needed. Read more • To make a link in an h1expo app open in a modal, add ?presentation=modal • HTMX will not be applied if the request is inside a webview (user agent is h1expo). • HTMX will not be applied by default if the link is inside active admin. • For styling, use the .ui-button class to make a link a button, or .ui-link class for plain underline • For links styled as buttons, add hx-indicator='this' to the <a> tag and add a loading spinner inside with <%= inline_svg_tag("misc/spinner.svg",class:"shown-while-loading") %>

Forms

• Use the form_for or form_with helper to open a form. Always add the .ui-form class. For example: <%= form_for @user, url: demo_user_form_path(@user), html: {class:"ui-form"} do |f| %> • For form fields, use the normal field helpers. For example: <%= f.text_field :first_name %> • Unless otherwise specified, use floating input labels for form elements. For example:

<%= f.text_field :first_name %><%= f.label :first_name, class:"--label" %> • By default, all forms submit asynchronously (because we have hx-boost on the body. To disable this behaviour, add hx-boost='false' to the form. • For nested forms, use accepts_nested_attributes_for in the model and fields_for in the view. To only display a subset of records, pass them in explicitly. For example: <%= user_form.fields_for :categories, @editable_categories do |nested_form| %> • Unless otherwise specified, include the form errors partial to show errors. For example: <%= render partial: "shared/form_errors", locals: { record: f.object} %> • Unless otherwise specified, add loading spinner icons to form submit buttons, with the .shown-while-loading class. For example: <%= f.button class: "ui-button --solid" do |button| %>Continue<%= inline_svg_tag("misc/spinner.svg", class:"shown-while-loading") %><% end %> • Use the same controller action for both get and post actions. For multistep forms, create an action for each step and redirect the user to the next step if saved successfully. For example: def get_order; if request.post?; redirect_to order_step2_path if @order.update(order_params); end; end • For multistep forms apply conditional validations by using an attr_accessor called validation_set. Set it in the controller just before a save, and add the validations in the model with a proc. For example: validates_presence_of :first_name, if: proc { |order| order.validation_set == "step1" } • For forms with nested records, use a new button to add records, and add hx-preserve to the existing record divs so that they don’t get overwritten. • For forms that require frontend notification messages, set toasts in the controller. For example: flash.now[:toasts] = [{ title: 'Post Created', message: 'Your post has been created.' }]

General

• All buttons should have loading spinners unless otherwise specified.

Last updated