- Switch to ActiveAdmin 4 beta with Tailwind CSS - Replace Sprockets with Propshaft for Rails 8 - Migrate from jQuery to vanilla JavaScript - Implement proper module initialization without setTimeout hacks - Add esbuild for JavaScript bundling with clean import aliases - Configure Tailwind CSS build process with ActiveAdmin plugin - Update engine configuration for both Propshaft and Sprockets - Add comprehensive documentation for AA4 gem updates - Maintain backward compatibility with legacy AA versions 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
26 KiB
ActiveAdmin 4 Migration Guide for Gem Maintainers
Overview
This document provides a comprehensive guide for gem maintainers on how to update their gems to support ActiveAdmin 4, based on the changes made to the activeadmin-searchable_select gem. The migration involved addressing significant changes in asset handling, JavaScript module systems, dependency management, and CSS selectors.
Key Migration Steps
1. Update Dependency Constraints
Ruby Version Requirements
- Minimum Ruby version: 3.2 (Ruby 3.0 and 3.1 are dropped in ActiveAdmin 4)
- Update your gemspec:
spec.required_ruby_version = '>= 3.2'
Rails Version Requirements
- Minimum Rails version: 7.0 (Rails 6.1 support dropped)
- ActiveAdmin 4 supports Rails 7.x and 8.x
ActiveAdmin Version
# In gemspec
spec.add_runtime_dependency 'activeadmin', ['>= 1.x', '< 5']
2. Asset Pipeline Migration
ActiveAdmin 4 moved away from the traditional Rails asset pipeline to modern JavaScript bundlers.
Key Changes:
- ActiveAdmin 4 assumes
cssbundling-railsandimportmap-railsare installed - No longer uses
register_stylesheetorregister_javascriptmethods - Requires explicit JavaScript module initialization
CSS bundling pattern (Rails 7 cssbundling + Tailwind)
- Build CSS to
app/assets/builds/active_admin.cssand expose it viaapp/assets/config/manifest.js://= link_tree ../builds//= link active_admin.css//= link active_admin.js//= link trumbowyg/icons.svg(when using Trumbowyg)
- Keep a single Tailwind config at the Rails app root (avoid duplicates). Using ESM works well:
tailwind.config.mjswithimport activeAdminPlugin from '@activeadmin/activeadmin/plugin'
- Source file
app/assets/stylesheets/active_admin_source.csscontains Tailwind directives, gem overrides and imports. - If Tailwind CLI does not inline vendor
@importfromnode_modules, concatenate vendor CSS before building. Example build script:
// spec/internal/package.json
{
"scripts": {
"build:css": "node ./build_css.js"
}
}
// spec/internal/build_css.js
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const root = __dirname;
const inputPath = path.join(root, 'app/assets/stylesheets/active_admin_source.css');
const vendorCssPath = path.join(root, 'node_modules/trumbowyg/dist/ui/trumbowyg.css');
const tmpPath = path.join(root, 'app/assets/stylesheets/__aa_tmp.css');
const outPath = path.join(root, 'app/assets/builds/active_admin.css');
const src = fs.readFileSync(inputPath, 'utf8').split(/\r?\n/);
const vendorCss = fs.readFileSync(vendorCssPath, 'utf8');
const tailwind = ['@tailwind base;','@tailwind components;','@tailwind utilities;'].join('\n');
const body = src.slice(3).filter(l => !l.includes('trumbowyg.css')).join('\n');
fs.writeFileSync(tmpPath, `${tailwind}\n\n${vendorCss}\n\n${body}`);
spawnSync('npx', ['tailwindcss','-c', path.join(root,'tailwind.config.mjs'),'-i', tmpPath,'-o', outPath], { stdio: 'inherit', cwd: root });
fs.unlinkSync(tmpPath);
This ensures vendor CSS (e.g., Trumbowyg) ships inside the built active_admin.css while keeping Tailwind at the top of the cascade so overrides behave as expected.
JavaScript Module Support
Create multiple module formats to support different bundlers:
- ESM Module (
your_gem.esm.js):
import $ from 'jquery';
import select2 from 'select2'; // Or your jQuery plugin
// Critical: Initialize jQuery plugins on the jQuery object for production builds
// This ensures the plugin methods are available on jQuery selections
select2($);
// Ensure jQuery is globally available for other scripts
window.$ = window.jQuery = $;
// Your initialization code wrapped in a DOM ready handler
$(() => {
// Initialize your plugin on specific selectors
$('.your-selector').yourPlugin({
// plugin options
});
// Listen for Turbo/Turbolinks events for dynamic content
$(document).on('turbo:load turbolinks:load', () => {
$('.your-selector').yourPlugin();
});
// For ActiveAdmin's dynamic content (filters, forms)
$(document).on('has_many_add:after', '.has_many_container', () => {
$('.your-selector').yourPlugin();
});
});
// Export for use as a module
export default function initializeYourGem() {
// Initialization logic
}
- Traditional Module (
your_gem.jsfor backward compatibility):
//= require jquery
//= require select2
(function($) {
'use strict';
$(document).ready(function() {
$('.your-selector').yourPlugin();
});
// Turbolinks/Turbo support
$(document).on('turbo:load turbolinks:load', function() {
$('.your-selector').yourPlugin();
});
})(jQuery);
- CDN-compatible version (for importmap users):
// Assumes jQuery and plugins are loaded via CDN
(() => {
'use strict';
const $ = window.jQuery || window.$;
if (!$) {
console.error('jQuery is required for YourGem');
return;
}
// Wait for DOM ready
$(() => {
$('.your-selector').yourPlugin();
});
})();
3. Installation Generator
Create a generator to help users set up your gem with different bundlers:
module YourGem
module Generators
class InstallGenerator < Rails::Generators::Base
class_option :bundler,
type: :string,
default: 'esbuild',
enum: %w[esbuild importmap webpack]
def setup_javascript
case options[:bundler]
when 'esbuild'
setup_esbuild
when 'importmap'
setup_importmap
when 'webpack'
setup_webpack
end
end
private
def setup_esbuild
# Add imports to app/javascript/active_admin.js
append_to_file 'app/javascript/active_admin.js' do
<<~JS
import $ from 'jquery';
import yourPlugin from 'your-plugin';
// Initialize plugin on jQuery
yourPlugin($);
window.$ = window.jQuery = $;
import '@your-scope/your-gem';
JS
end
end
def setup_importmap
# Add pins to config/importmap.rb
append_to_file 'config/importmap.rb' do
<<~RUBY
pin "jquery", to: "https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
pin "your-plugin", to: "https://cdn.jsdelivr.net/npm/your-plugin/dist/plugin.min.js"
pin "your-gem", to: "your-gem.js"
RUBY
end
end
end
end
end
4. NPM Package Publishing
If your gem includes JavaScript, consider publishing an NPM package:
Package.json Configuration
{
"name": "@activeadmin/your-gem",
"version": "1.0.0",
"description": "Your gem description for ActiveAdmin",
"main": "src/index.js",
"module": "src/index.js",
"exports": {
".": {
"import": "./src/index.js",
"require": "./src/index.js",
"default": "./src/index.js"
},
"./css": "./src/styles.scss"
},
"peerDependencies": {
"jquery": ">= 3.0, < 5",
"select2": "^4.0.13" // Add your dependencies here
},
"files": [
"src/**/*",
"app/assets/**/*",
"vendor/assets/**/*"
],
"scripts": {
"prepare_sources": "mkdir -p src && cp -r app/assets/javascripts/active_admin/* src/ && cp -r app/assets/stylesheets/active_admin/* src/",
"prepublishOnly": "npm run prepare_sources"
},
"repository": {
"type": "git",
"url": "https://github.com/your-org/your-gem.git"
},
"keywords": ["activeadmin", "rails", "your-feature"],
"author": "Your Name",
"license": "MIT"
}
Preparing JavaScript Assets for NPM
Create a script to copy your assets to the NPM package structure:
#!/bin/bash
# scripts/prepare_npm_package.sh
# Create src directory for NPM
mkdir -p src
# Copy JavaScript files
cp -r app/assets/javascripts/active_admin/* src/
# Copy SCSS files if needed
cp -r app/assets/stylesheets/active_admin/* src/
# Ensure ESM module is included
cp app/assets/javascripts/active_admin/your_gem.esm.js src/index.js
5. CSS Selector Updates
ActiveAdmin 4 introduced several CSS class changes:
| ActiveAdmin 3.x | ActiveAdmin 4.x |
|---|---|
.filter_form |
.filters-form |
.tabs component |
Removed - use divs with Tailwind |
.columns component |
Replaced with Tailwind grid |
Update your JavaScript and CSS accordingly:
// Old
$('.filter_form select').select2();
// New
$('.filters-form select').select2();
6. Testing Updates with Combustion
Complete Combustion Workflow for ActiveAdmin 4 Gems
CRITICAL: This workflow is specifically for testing ActiveAdmin extension gems with Combustion.
Step 1: Add Dependencies to Gemfile
# Gemfile (for development/testing)
gem 'combustion'
gem 'importmap-rails', '~> 2.0' # Required for ActiveAdmin 4
Step 2: Run Combustion Generator (MANDATORY!)
# NEVER manually create spec/internal structure!
bundle exec combust
This creates:
spec/internal/- minimal Rails app structureconfig.ru- in gem root forbundle exec rackup- Basic Rails directories and config files
Step 3: Set Up Test App Structure
After generator, create these files:
# Create necessary directories
mkdir -p spec/internal/app/models
mkdir -p spec/internal/app/admin
mkdir -p spec/internal/app/assets/stylesheets
mkdir -p spec/internal/app/javascript
mkdir -p spec/internal/config/initializers
Basic Combustion Configuration
# spec/rails_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require 'combustion'
# Initialize Combustion with only needed components
Combustion.path = 'spec/internal'
Combustion.initialize!(:active_record, :action_controller, :action_view) do
config.load_defaults Rails::VERSION::STRING.to_f if Rails::VERSION::MAJOR >= 7
end
require 'rspec/rails'
require 'capybara/rails'
Step 4: Configure config.ru (CRITICAL Loading Order!)
# config.ru - MUST control loading order for ActiveAdmin!
require "rubygems"
require "bundler"
# DON'T use Bundler.require - it loads gems too early!
Bundler.setup(:default, :development)
# Load Rails and combustion first
require 'combustion'
# Initialize Combustion with Rails components
Combustion.initialize! :active_record, :action_controller, :action_view do
config.load_defaults Rails::VERSION::STRING.to_f if Rails::VERSION::MAJOR >= 7
end
# NOW we can load ActiveAdmin and its dependencies after Rails is initialized
require 'importmap-rails'
require 'active_admin'
require 'your_activeadmin_gem'
run Combustion::Application
Step 5: Configure ActiveAdmin Assets
# spec/internal/app/assets/stylesheets/active_admin.css
@tailwind base;
@tailwind components;
@tailwind utilities;
# spec/internal/config/importmap.rb
pin "@activeadmin/activeadmin", to: "active_admin.js", preload: true
// spec/internal/app/javascript/active_admin.js
// Placeholder for ActiveAdmin JS
console.log("ActiveAdmin loaded");
Step 6: Set Up Test Models and Admin Resources
# spec/internal/db/schema.rb
ActiveRecord::Schema.define do
create_table :active_admin_comments, force: true do |t|
t.string :namespace
t.text :body
t.references :resource, polymorphic: true
t.references :author, polymorphic: true
t.timestamps
end
create_table :posts, force: true do |t|
t.string :title
t.text :body
t.text :description
t.timestamps
end
end
# spec/internal/config/routes.rb
Rails.application.routes.draw do
ActiveAdmin.routes(self)
root to: 'admin/dashboard#index'
end
# spec/internal/config/initializers/active_admin.rb
ActiveAdmin.setup do |config|
config.site_title = "Test App"
config.authentication_method = false
config.current_user_method = false
config.batch_actions = true
end
Step 7: Configure rails_helper.rb
# spec/rails_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require 'combustion'
Combustion.path = 'spec/internal'
Combustion.initialize!(:active_record, :action_controller, :action_view) do
config.load_defaults Rails::VERSION::STRING.to_f if Rails::VERSION::MAJOR >= 7
end
require 'rspec/rails'
require 'capybara/rails'
Step 8: Running the Test App
# Start the test app server
bundle exec rackup
# Visit http://localhost:9292/admin
Critical Testing Pitfall: Model Registration Conflicts
Problem: Dynamic ActiveAdmin registrations in tests conflict with static admin files.
Solution: Choose ONE approach per model:
- Static Registration (for consistent configs):
# spec/internal/app/admin/users.rb
ActiveAdmin.register User do
permit_params :name, :email
# Fixed configuration
end
- Dynamic Registration (for varying configs):
# spec/support/active_admin_helpers.rb
module ActiveAdminHelpers
module_function
def setup
ActiveAdmin.application = nil
yield # Dynamic registration block
reload_routes!
end
def reload_routes!
Rails.application.reload_routes!
end
end
# In test - NO static admin file for Post model
ActiveAdminHelpers.setup do
ActiveAdmin.register(Post) do
# Test-specific configuration
end
end
Important: Never mix static and dynamic registration for the same model!
Capybara Configuration with Playwright
# spec/support/capybara.rb
require 'capybara-playwright-driver'
Capybara.register_driver :playwright do |app|
Capybara::Playwright::Driver.new(
app,
browser_type: :chromium,
headless: true,
viewport: { width: 1920, height: 1080 }
)
end
Capybara.default_driver = :rack_test
Capybara.javascript_driver = :playwright
# Important: Set server for JS tests
Capybara.server = :puma, { Silent: true }
Waiting for JavaScript/AJAX in Tests
# spec/support/wait_helpers.rb
module WaitHelpers
def wait_for_ajax
Timeout.timeout(Capybara.default_max_wait_time) do
sleep 0.1
loop until finished_all_ajax_requests?
end
end
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
# For Select2 or similar plugins
def wait_for_select2
expect(page).to have_css('.select2-container', wait: 5)
end
end
RSpec.configure do |config|
config.include WaitHelpers, type: :feature
end
7. Production Build Issues
Common production issues and solutions:
Issue: JavaScript plugin not initialized
Solution: Explicitly initialize jQuery plugins
import select2 from 'select2';
import $ from 'jquery';
// This is critical for production builds
select2($);
Issue: jQuery not globally available
Solution: Ensure global assignment
window.$ = window.jQuery = $;
Issue: Assets not loading in production
Solution: Use CDN fallbacks or vendor assets
# In your gem's engine.rb
class Engine < ::Rails::Engine
initializer 'your_gem.assets' do |app|
if Rails.env.production?
# Add fallback assets
end
end
end
8. CI/CD Updates
Update your GitHub Actions workflow:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: ['3.2', '3.3']
rails: ['7.0', '7.1', '7.2', '8.0']
activeadmin: ['4.0.0.beta16']
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install npm dependencies
run: npm install
- name: Install Playwright browsers
run: npx playwright install chromium
- name: Run tests
run: bundle exec rspec
9. Appraisals Configuration
Use Appraisal gem to test against multiple versions:
# Appraisals file
appraise 'rails-7.x-active-admin-4.x' do
gem 'rails', '~> 7.0'
gem 'activeadmin', '~> 4.0.0.beta16'
gem 'propshaft' # Required - Sprockets not supported
end
appraise 'rails-8.x-active-admin-4.x' do
gem 'rails', '~> 8.0'
gem 'activeadmin', '~> 4.0.0.beta16'
# Rails 8 includes Propshaft by default
end
10. Common Pitfalls and Solutions
Pitfall 1: Select2 or similar jQuery plugins not working
Root Cause: Plugin not attached to jQuery object in production
Solution: Explicitly call plugin($) after importing
import select2 from 'select2';
import $ from 'jquery';
select2($); // Critical - attaches plugin to jQuery
Pitfall 2: CSS classes not found
Root Cause: ActiveAdmin 4 changed many CSS selectors Solution: Search and replace old selectors with new ones
.filter_form→.filters-form.select2-containerneeds explicit initialization in tests
Pitfall 3: Tests passing locally but failing in CI
Root Cause: Missing JavaScript dependencies or browser drivers Solution:
# .github/workflows/ci.yml
- name: Install Playwright browsers
run: npx playwright install chromium
Pitfall 4: Assets not compiling in production
Root Cause: Missing bundler configuration Solution: Provide clear setup instructions for each bundler type in your README
Pitfall 5: Model registration conflicts in tests
Root Cause: Static admin files override dynamic test registrations Solution:
- Delete static admin files for models that need dynamic config
- Keep static files only for models with consistent config
- Never mix both approaches for the same model
Pitfall 6: Input HTML options not passing through
Root Cause: Options can be lost during form DSL processing Solution: Test with clean models not affected by other registrations
# Test with a model that has no static admin file
ActiveAdmin.register(TestModel) do
form do |f|
f.input :field, as: :searchable_select,
input_html: { class: 'custom-class' }
end
end
Pitfall 7: Flaky JavaScript tests
Root Cause: Not waiting for AJAX/DOM updates Solution: Add proper wait helpers
def wait_for_select2
expect(page).to have_css('.select2-container', wait: 5)
end
Pitfall 8: Rails 8 compatibility issues
Root Cause: Formtastic 5.0 changes, Ransack updates Solution:
- Test against multiple Rails versions using Appraisal
- Ensure Ransack methods are defined in models
def self.ransackable_attributes(_auth_object = nil)
%w[name title]
end
Pitfall 9: Combustion and ActiveAdmin Loading Order Issues
Root Cause: ActiveAdmin requires Rails components at load time, conflicts with Combustion's initialization
Critical Issue: ActiveAdmin's Bundler.require loads before Rails is initialized by Combustion
Symptoms:
uninitialized constant Formtastic::ActionViewuninitialized constant ActiveSupport::Autoloaduninitialized constant #<Class:ActiveAdmin>::Importmap- Rackup fails with various Rails component loading errors Solution:
- Use
Bundler.setupinstead ofBundler.requirein config.ru - Load ActiveAdmin AFTER Combustion initializes Rails
- Include importmap-rails for ActiveAdmin 4
- Don't require ActiveAdmin components in gem's main file
# Bad: In lib/your_gem.rb
require 'active_admin' # This loads too early!
require 'formtastic/inputs/your_input'
# Good: In engine.rb
initializer 'your_gem.setup', after: :load_config_initializers do
require 'active_admin' if defined?(Rails.application)
ActiveSupport.on_load(:active_admin) do
require 'formtastic/inputs/your_input'
end
end
Pitfall 10: ActiveAdmin 4 Asset Pipeline Requirements (CRITICAL FOR COMBUSTION GEMS)
Root Cause: ActiveAdmin 4 uses Tailwind CSS v3 with custom plugin, requires compilation Critical Issue: CSS must be compiled through Tailwind with ActiveAdmin plugin Symptoms:
- Unstyled admin pages (no proper layout, just basic HTML)
- CSS file exists but has 0 bytes or wrong content
The asset "active_admin.css" is not present in the asset pipeline
Complete Solution for Combustion-based Gems:
- Add Dependencies (Gemfile):
gem 'importmap-rails', '~> 2.0'
gem 'tailwindcss-rails' # For bundled tailwindcss executable
- Install NPM packages (in spec/internal):
cd spec/internal
npm init -y
npm install --save-dev tailwindcss@^3 # Use v3, not v4!
npm install --save-dev @activeadmin/activeadmin # For plugin (optional)
- Copy ActiveAdmin Plugin (from Ruby gem):
cp $(bundle show activeadmin)/plugin.js spec/internal/activeadmin-plugin.js
- Create Tailwind Config (spec/internal/tailwind.config.mjs):
import activeAdminPlugin from './activeadmin-plugin.js';
import { execSync } from 'child_process';
const activeAdminPath = execSync('bundle show activeadmin', { encoding: 'utf-8' }).trim();
export default {
content: [
`${activeAdminPath}/app/views/**/*.{arb,erb,html,rb}`,
'./app/admin/**/*.{arb,erb,html,rb}',
'./app/views/**/*.{arb,erb,html}',
'./app/javascript/**/*.js'
],
darkMode: 'selector',
plugins: [activeAdminPlugin]
}
- Create Source CSS (spec/internal/app/assets/stylesheets/active_admin_source.css):
@tailwind base;
@tailwind components;
@tailwind utilities;
- Build CSS:
cd spec/internal
npx tailwindcss -c tailwind.config.mjs \
-i app/assets/stylesheets/active_admin_source.css \
-o app/assets/stylesheets/active_admin_compiled.css \
--minify
- Configure Sprockets (spec/internal/app/assets/stylesheets/active_admin.css):
/*
* This imports the compiled Tailwind CSS with ActiveAdmin styles
*= require ./active_admin_compiled
*/
- Update Manifest (spec/internal/app/assets/config/manifest.js):
//= link_tree ../builds
//= link active_admin.css
- Create Build Task (lib/tasks/active_admin.rake):
namespace :active_admin do
desc 'Build Active Admin Tailwind stylesheets'
task :build do
require 'fileutils'
input = File.expand_path('../../spec/internal/app/assets/stylesheets/active_admin_source.css', __dir__)
output = File.expand_path('../../spec/internal/app/assets/stylesheets/active_admin_compiled.css', __dir__)
config = File.expand_path('../../spec/internal/tailwind.config.mjs', __dir__)
FileUtils.mkdir_p(File.dirname(output))
command = ['npx', 'tailwindcss', '-c', config, '-i', input, '-o', output, '--minify']
puts "Building Tailwind CSS: #{command.join(' ')}"
system(*command, exception: true)
puts "Tailwind CSS build complete: #{output}"
end
end
Pitfall 11: Formtastic Custom Inputs Not Loading in Combustion
Root Cause: Loading order issues with ActiveAdmin, Formtastic, and custom inputs Symptoms:
Formtastic::UnknownInputError: Unable to find input class YourInput- Input works in production but not in Combustion test environment
Solutions:
- Immediate Fix in config.ru (for Combustion):
# config.ru
require 'combustion'
Combustion.initialize! :active_record, :action_controller, :action_view
require 'importmap-rails'
require 'active_admin'
require 'your_gem'
# Critical: Explicitly require custom inputs after everything else
require 'formtastic/inputs/your_input'
run Combustion::Application
- Engine Initialization Fix:
# lib/your_gem/engine.rb
initializer 'your_gem.setup', after: :load_config_initializers do
require 'active_admin' if defined?(Rails.application)
# Load immediately AND hook into ActiveAdmin
require 'formtastic/inputs/your_input'
ActiveSupport.on_load(:active_admin) do
require 'formtastic/inputs/your_input'
end
end
- Workaround Using Standard Inputs:
# If custom input isn't loading, use standard input with same attributes
f.input :field, as: :text, input_html: {
class: 'your-input-class',
'data-your-attribute': true
}
Note: After making these changes, restart the server for them to take effect.
Migration Checklist
- Update Ruby version requirement to >= 3.2
- Update Rails version requirement to >= 7.0
- Update ActiveAdmin dependency to support 4.x
- Create ESM JavaScript modules
- Add installation generator for different bundlers
- Publish NPM package (if applicable)
- Update CSS selectors (
.filter_form→.filters-form) - Fix jQuery plugin initialization for production
- Update test suite for new asset handling
- Configure CI for multiple version testing
- Update documentation with setup instructions
- Test with esbuild, webpack, and importmap
- Add CDN fallbacks for JavaScript dependencies
- Handle both Sprockets and Propshaft
Example Implementation
See the full implementation in the activeadmin-searchable_select gem:
Resources
- ActiveAdmin 4.0 Breaking Changes
- ActiveAdmin 4.0 Release Notes
- Rails 7+ Asset Pipeline Guide
- esbuild Rails Documentation
- Importmap Rails Documentation
Conclusion
Migrating a gem to support ActiveAdmin 4 requires careful attention to:
- Modern JavaScript module systems
- Flexible asset pipeline support
- Updated CSS selectors and components
- Proper jQuery plugin initialization
- Comprehensive testing across different setups
The key to success is providing multiple paths for users with different asset pipeline configurations while maintaining backward compatibility where possible.