Jason Chee

Background

I am a fullstack web developer with 7+ years experience building web products while being hands-on in each layer of the product stack—from planning to execution and shipping. I’ve worked exclusively with Ruby and Ruby on Rails throughout my career.

Read more about me

Notes

May 03, 2025

Enums with sqlite

At the time of this article SQLite does not support the enum column type. We can still define and use enums in Active Record with the enum helper which was introduced in Rails 7.

We can define the column using Integer or String. In the following example, I will use strings. If you choose to use Integer you’ll have to update the check_contraint.

class CreateUsers < ActiveRecord::Migration[8.0]
  def change
    create_table :users do |t|
      t.string :email_address, null: false
      t.string :password_digest, null: false

      t.string :role, null: false, default: "admin"
      t.check_constraint "role IN ('admin', 'instructor', 'student')", name: "role_check"

      t.timestamps
    end
    
    add_index :users, :email_address, unique: true
    add_index :users, :role
  end
end

Next, I will define those roles with the enum helper in the User model.

class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  enum :role, { admin: "admin", instructor: "instructor", student: "student" }, prefix: true

  normalizes :email_address, with: ->(e) { e.strip.downcase }

  validates :role, presence: true
end

Defining an enum in Active Record provides useful instance methods. We will now be able to call:

User.role_admin? User.not_admin

user = User.create!
user.role
# => "admin"
user.role_student!
user.role
# => "student"

April 28, 2025

Stimulus outlets

Stimulus Outlets is a superful useful feature that allows one controller talk or interact with another controller that lives elsewhere in the page — even if they are not nested together.

This can be really helpful when you want components on your page to interact without tightly coupling their HTML structure.

Let’s say a user is browsing through products on the /products page. Here’s a simplified example:

<!-- products/index.html.erb -->

<div class="mx-auto max-w-2xl mt-24" data-controller="cart" data-cart-cart-notification-outlet=".cart-count">
	<div class="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
		<% 4.times do %>
			<a href="#" class="group">
		      <img src="https://tailwindcss.com/plus-assets/img/ecommerce-images/category-page-04-image-card-01.jpg" class="aspect-square w-full rounded-lg bg-gray-200 object-cover group-hover:opacity-75 xl:aspect-7/8">
		      <h3 class="mt-4 text-sm text-gray-700">Earthen Bottle</h3>
		      <p class="mt-1 text-lg font-medium text-gray-900">$48</p>
		      <button class="mt-1 bg-blue-700 hover:bg-blue-600 flex py-2 w-full rounded-4xl justify-center text-white font-light cursor-pointer text-sm" data-action="cart#add:prevent">Add to cart</button>
		    </a>
	   <% end %>
	</div>
</div>
// cart_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static outlets = [ "cart-notification" ]

  add() {
    if (this.hasCartNotificationOutlet) {
      this.cartNotificationOutlet.updateCount()
    }
  }
}

Whenever a user clicks “Add to cart,” we want to update the cart count badge. However, the cart badge is rendered separately — inside the layout, not inside the /products page itself.

<!-- layouts/application.html.erb -->

<body class="min-h-full flex flex-col">
  <div class="flex-1">
    <%= yield %>
    <%= render "layouts/cart_count" %>
  </div>
</body>
<!-- layouts/_cart_count.html.erb -->
<div class="cart-count" data-controller="cart-notification">
	<div class="hidden fixed bottom-5 right-7" data-cart-notification-target="container">
	  <div class="relative py-2">
	    <div class="t-0 absolute left-3">
	      <p class="flex h-2 w-2 items-center justify-center rounded-full bg-red-500 p-3 text-xs text-white" data-cart-notification-target="itemsCount"></p>
	    </div>
	    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="file: mt-4 h-6 w-6">
	      <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
	    </svg>
	  </div>
  </div>
</div>
// cart_notification_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["container", "itemsCount"]

  connect() {
    this.cartItems = 0
  }

  updateCount(event) {
    if (this.containerTarget.classList.contains("hidden") && this.cartItems == 0) {
      this.containerTarget.classList.remove("hidden");
    };

    this.cartItems += 1;
    this.itemsCountTarget.innerText = this.cartItems;
  }
}

Since the cart controller (cart_controller.js) lives in the /products page and the cart notification controller (cart_notification_controller.js) lives globally inside the layout, outlets allow the cart controller to find and update the cart notification controller easily — without needing to nest them together or manually query the DOM.

Similar to Targets, Outlets have callbacks that allow you to run code whenever an outlet is connected or disconnected from the page.

This is incredibly powerful because it lets your controller react dynamically as the structure of the page changes — without needing to manually observe the DOM yourself.

For example, imagine you’re building a chat application. When a user receives a new incoming message like “Happy Birthday!”, you could have that message element matched by an outlet selector like .celebration. When the outlet connects, you could automatically trigger a fun confetti animation or display a special effect — all driven by the outlet connection itself, without any extra manual wiring.

Some other use cases where outlets might be useful(although most of these could also be acheived through ruby):

A form wizard with a current-step navigation bar - Inform the user what current step they are on.

Dynamic sidebars - Selecting something in the main content area updates sidebar content dynamically.

Modal openers - Buttons that open modals “from far away” without having the modal HTML nested inside the button.

Flash notifications - A form submits → triggers a global “success” notification banner to appear.


April 20, 2025

One man band starter pack

a friendly reminder that this might be all you really need:

basics ruby on rails 8, esbuild/vite, sqlite3/postgresql, turbo, stimulus, active_job, solid_queue, solid_cache, active_storage, action_text, active_admin/administrate

frontend/ui/ux tailwind css, bootstrap 5, lookbook, viewcomponent/phlex, figma

testing/logging minitest, debugbar, appsignal, capybara, selenium, factory_bot

apis stripe, sendgrid, rubyllm(openai, ollama)

deploy/devops hatchbox, digitalocean/hetzner, stripe, sendgrid, twilio

marketing webflow?

other hotwire native, fastlane, apple developer account, google play account


March 25, 2025

Polymorphic associations checklist

When you’re unsure to use a polymorphic association:

Does this table belong to multiple, unrelated models?

👍 good use case: a Notification might belong to a Task, Comment, or a User.

👎 bad use case: a Book belongs to an Author…this is a regular belongs_to relationship

Do I need a flexible relationship?

You want a single association that can reference different models dynamically. You don’t want to add a new foreign key each time you need a new type of association.

example: instead of adding post_id, product_id, and event_id to Taggings, you use taggable_type and taggable_id.

Is this relationship likely to grow in complexity over time?

You anticipate that this association will be needed by many different models now or in the future.

example: a Tag might eventually need to attach to a Post, Photo, Video, or Event.

Do I want to avoid schema changes?

You want to avoid altering the schema every time a new model needs to use the association. Polymorphic associations let you add new relationships without additional migrations

When to avoid polymorphic associations

• If the associated models are closely related or known to be limited in number.

• If you need JOIN queries across polymorphic types.
example: since commentable_type varies row-to-row(ex: a Post commentable_type, or a Photo commentable_type) you are unable to use a single SQL JOIN.

• If you need foreign key constraints.

Lets create a polymorphic association!

create migration:

# db/migrate/20240411123456_create_comments.rb
class CreateComments < ActiveRecord::Migration[7.0]
  def change
    create_table :comments do |t|
      t.text :body
      t.references :commentable, polymorphic: true, null: false

      t.timestamps
    end
  end
end

model setup:

# models/comment.rb
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end
# models/post.rb
class Post < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end
# models/photo.rb
class Photo < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end

usage example:

post = Post.create(title: "First Post")
photo = Photo.create(caption: "Nice view!")

# add comments to each
post.comments.create(body: "Great post!")
photo.comments.create(body: "Awesome picture!")

# fetch all comments
Comment.all.each do |comment|
  puts "#{comment.body} on #{comment.commentable.class.name} ##{comment.commentable.id}"
end

March 13, 2024

JavaScript libaries, Vue.js, Ruby on Rails and importmaps.

Here is an example how you can import a JavaScript library to your Vue component when using importmaps in Ruby on Rails

Step 1 - Pin the library in importmaps

Pin your libaray in config/importmaps.rb

pin "axios", to: "https://cdn.skypack.dev/pin/axios@v1.6.7-CtiTWk1RHZKnFCXj0sDG/mode=imports,min/optimized/axios.js", preload: true

Step 2 - Reference the library in your component

// app/javascript/components/HelloWorld.js
import axios from "axios"

const HelloWorld = {
  template: `
    <div>
      <h1></h1>
    </div>
  `,
  data() {
    return {
      message: 'Loading...'
    };
  },
	methods: {
    async fetchData() {
      try {
        const response = await axios.get('https://pokeapi.co/api/v2/pokemon/ditto');
        console.log(response);
        this.message = response.data.name
        // Update your component's data with the fetched data
      } catch (error) {
        console.error('There was an error fetching the data:', error);
        this.message = error
      }
    }
  },
	mounted() {
	  this.fetchData();
	},      
}

export default HelloWorld;