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.
- 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'
- Minimum Rails version: 7.0 (Rails 6.1 support dropped)
- ActiveAdmin 4 supports Rails 7.x and 8.x
# In gemspec
spec.add_runtime_dependency 'activeadmin', ['>= 1.x', '< 5']ActiveAdmin 4 moved away from the traditional Rails asset pipeline to modern JavaScript bundlers.
- ActiveAdmin 4 assumes
cssbundling-railsandimportmap-railsare installed - No longer uses
register_stylesheetorregister_javascriptmethods - Requires explicit JavaScript module initialization
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();
});
})();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
endIf your gem includes JavaScript, consider publishing an NPM package:
{
"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"
}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.jsActiveAdmin 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();Combustion provides a minimal Rails app for testing engines. Here's the proper setup:
# 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'spec/internal/
├── app/
│ ├── models/ # Test models (auto-loaded by Rails)
│ └── admin/ # Static ActiveAdmin registrations
├── config/
│ ├── database.yml
│ ├── routes.rb
│ └── initializers/
│ └── active_admin.rb
└── db/
└── schema.rb # Define test database structure
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
endImportant: Never mix static and dynamic registration for the same model!
# 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 }# 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
endCommon production issues and solutions:
Solution: Explicitly initialize jQuery plugins
import select2 from 'select2';
import $ from 'jquery';
// This is critical for production builds
select2($);Solution: Ensure global assignment
window.$ = window.jQuery = $;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
endUpdate 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 rspecUse 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 'sassc-rails'
gem 'sprockets', '~> 4.0'
end
appraise 'rails-8.x-active-admin-4.x' do
gem 'rails', '~> 8.0'
gem 'activeadmin', '~> 4.0.0.beta16'
gem 'sassc-rails'
gem 'propshaft'
endRoot 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 jQueryRoot 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
Root Cause: Missing JavaScript dependencies or browser drivers Solution:
# .github/workflows/ci.yml
- name: Install Playwright browsers
run: npx playwright install chromiumRoot Cause: Missing bundler configuration Solution: Provide clear setup instructions for each bundler type in your README
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
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
endRoot 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)
endRoot 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- 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
See the full implementation in the activeadmin-searchable_select gem:
- ActiveAdmin 4.0 Breaking Changes
- ActiveAdmin 4.0 Release Notes
- Rails 7+ Asset Pipeline Guide
- esbuild Rails Documentation
- Importmap Rails Documentation
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.