@rocket-sensei/activeadmin-searchable_select
Version:
Use searchable selects based on Select2 in Active Admin forms and filters.
440 lines (339 loc) • 12 kB
Markdown
# ActiveAdmin Searchable Select (RocketSensei Fork)
[](http://badge.fury.io/rb/rs-activeadmin-searchable_select)
[](https://badge.fury.io/js/@rocket-sensei%2Factiveadmin-searchable_select)
[](https://www.npmjs.com/package/@rocket-sensei/activeadmin-searchable_select)
[](https://github.com/glebtv/activeadmin-searchable_select/actions)
Searchable select boxes (via [Select2](https://select2.org/)) for
ActiveAdmin forms and filters. Extends the ActiveAdmin resource DSL to
allow defining JSON endpoints to fetch options from asynchronously.
## Installation
Add `rs-activeadmin-searchable_select` to your Gemfile:
```ruby
gem 'rs-activeadmin-searchable_select'
```
### ActiveAdmin 4.x with Rails 8 (esbuild/importmap/Propshaft)
This fork is optimized for ActiveAdmin 4.x with Rails 8, supporting modern JavaScript bundlers and Propshaft. If you encounter "select2 is not a function" errors in production, see the [complete setup guide](docs/guide-update-your-app.md).
#### Quick Install with Generator
```bash
# For esbuild (recommended)
rails generate active_admin:searchable_select:install
# For importmap
rails generate active_admin:searchable_select:install --bundler=importmap
```
#### Manual Setup for esbuild
1. Install npm packages:
```bash
npm install @rocket-sensei/activeadmin-searchable_select jquery select2
```
2. In `app/javascript/active_admin.js`:
```javascript
import "@activeadmin/activeadmin";
import $ from 'jquery';
import select2 from 'select2';
// Critical: Initialize select2 on jQuery (fixes production builds)
select2($);
window.$ = window.jQuery = $;
import '@rocket-sensei/activeadmin-searchable_select';
```
3. Add Select2 CSS to your ActiveAdmin stylesheet:
```css
@import url('https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css');
```
### ActiveAdmin 3.x and older
##### Using assets via Sprockets
Import stylesheets and require javascripts:
```scss
// active_admin.css.scss
@import "active_admin/searchable_select";
```
```coffee
// active_admin.js
//= require active_admin/searchable_select
```
##### Using assets via Webpacker
Add to `package.json`:
```json
"dependencies": {
"@rocket-sensei/activeadmin-searchable_select": "^4.0.0"
}
```
In `app/javascript/packs/active_admin.js`:
```javascript
import '@rocket-sensei/activeadmin-searchable_select';
```
In `app/javascript/stylesheets/active_admin.scss`:
```css
@import '@rocket-sensei/activeadmin-searchable_select';
```
## Usage
### Making Select Boxes Searchable
To add search functionality to a select box, use the
`:searchable_select` input type:
```ruby
ActiveAdmin.register Product do
form do |f|
f.input(:category, as: :searchable_select)
end
end
```
This also works for filters:
```ruby
ActiveAdmin.register Product do
filter(:category, as: :searchable_select)
end
```
By default, you can only select one at a time for a filter. You can
specify a multi-select with:
```ruby
ActiveAdmin.register Product do
filter(:category, as: :searchable_select, multiple: true)
end
```
### Fetching Options via Ajax
For large collections, rendering the whole set of options can be to
expensive. Use the `ajax` option to fetch a set of matching options
once the user begins to type:
```ruby
ActiveAdmin.register Product do
filter(:category,
as: :searchable_select,
ajax: true)
end
```
If the input attribute corresponds to an ActiveAdmin resource, it is
expected to provide the JSON endpoint that provides the options. Use
the `searchable_select_options` method to define the required
collection action:
```ruby
ActiveAdmin.register Category do
searchable_select_options(scope: Category.all,
text_attribute: :name)
end
```
By default, `scope` needs to be a Ransack enabled ActiveRecord
collection proxy determining which options are available. The
attribute given by `text_attribute` will be used to get a display name
for each record. Via Ransack, it is also used to filter by search
term. Limiting result set size is handled automatically.
You can customize the display text:
```ruby
ActiveAdmin.register Category do
searchable_select_options(scope: Category.all,
text_attribute: :name,
display_text: ->(record) { "Category: #{record.name}" } )
end
```
Note that `text_attribute` is still required to perform filtering via
Ransack. You can pass the `filter` option, to specify your own
filtering strategy:
```ruby
ActiveAdmin.register Category do
searchable_select_options(scope: Category.all,
text_attribute: :name,
filter: lambda |term, scope|
scope.ransack(name_cont_all: term.split(' ')).result
end)
end
```
`scope` can also be a lambda which is evaluated in the context of the
collection action defined by the helper:
```ruby
ActiveAdmin.register Category do
searchable_select_options(scope: -> { Category.allowed_for(current_user) },
text_attribute: :name)
end
```
If the input attribute is set on the form's object, ajax based
searchable selects will automatically render a single option to ensure
the selected item is displayed correctly even before options have been
loaded asynchronously.
#### Specifying the Options Endpoint Resource
If the resource that provides the options endpoint cannot be guessed
based on the input attribute name, you can pass an object with a
`resource` key as `ajax` option:
```ruby
ActiveAdmin.register Product do
form do |f|
f.input(:additional_category,
as: :searchable_select,
ajax: { resource: Category })
end
end
```
#### Multiple Options Endpoints per Resource
A single ActiveAdmin resource can define multiple options endpoints:
```ruby
ActiveAdmin.register Category do
searchable_select_options(name: :favorites,
scope: Category.favorites,
text_attribute: :name)
searchable_select_options(name: :recent,
scope: Category.recent,
text_attribute: :name)
end
```
To specify which collection to use, pass an object with a
`collection_name` key as `ajax` option:
```ruby
ActiveAdmin.register Product do
form do |f|
f.input(:category,
as: :searchable_select,
ajax: { collection_name: :favorites })
end
end
```
#### Querying Multiple Attributes
ActiveAdmin Searchable Select querying is performed by Ransack. As such, you can
build your query in a way that it can query multiple attributes at once.
```ruby
ActiveAdmin.register User do
searchable_select_options(scope: User.all,
text_attribute: :username,
filter: lambda do |term, scope|
scope.ransack(email_or_username_cont: term).result
end)
end
```
In this example, the `all` scope will query `email OR username`.
You can add the additional payload as dsl option:
```ruby
ActiveAdmin.register Category do
searchable_select_options(scope: Category.all,
text_attribute: :name,
additional_payload: ->(record) { { foo: record.bar } } )
end
```
response example which uses additional_payload:
```json
{
"results": [{ "id": "1", "text": "Bicycles", "foo": "Bar" }],
"pagination": { "more": "false" }
}
```
#### Passing Parameters
You can pass additional parameters to the options endpoint:
```ruby
ActiveAdmin.register Product do
form do |f|
f.input(:category,
as: :searchable_select,
ajax: {
params: {
some: 'value'
}
})
end
end
```
The lambda passed as `scope` can receive those parameters as first
argument:
```ruby
ActiveAdmin.register Category do
searchable_select_options(scope: lambda do |params|
Category.find_all_by_some(params[:some])
end,
text_attribute: :name)
end
```
#### Path options for nested resources
Example for the following setup:
```ruby
# Models
class OptionType < ActiveRecord::Base; end
class OptionValue < ActiveRecord::Base
belongs_to :option_type
end
class Product < ActiveRecord::Base
belongs_to :option_type
has_many :variants
end
class Variant < ActiveRecord::Base
belongs_to :product
belongs_to :option_value
end
# ActiveAdmin
ActiveAdmin.register(OptionType)
ActiveAdmin.register(Product)
ActiveAdmin.register(OptionValue) do
belongs_to :option_type
searchable_select_options(scope: lambda do |params|
OptionValue.where(
option_type_id: params[:option_type_id]
)
end,
text_attribute: :value)
end
```
It is possible to pass path parameters for correctly generating URLs for nested resources fetching via `path_params`
```ruby
ActiveAdmin.register(Variant) do
belongs_to :product
form do |f|
...
f.input(:option_value,
as: :searchable_select,
ajax: {
resource: OptionValue,
path_params: {
option_type_id: f.object.product.option_type_id
}
})
...
end
end
```
This will generate the path for fetching as `all_options_admin_option_type_option_values(option_type_id: f.object.product.option_type_id)` (e.g. `/admin/option_types/2/option_values/all_options`)
#### Inlining Ajax Options in Feature Tests
When writing UI driven feature specs (i.e. with Capybara),
asynchronous loading of select options can increase test
complexity. `activeadmin-searchable_select` provides an option to
render all available options just like a normal select input while
still exercsing the same code paths including `scope` and
`text_attribute` handling.
For example with RSpec/Capybara, simply set `inline_ajax_options` true
for feature specs:
```ruby
RSpec.configure do |config|
config.before(:each) do |example|
ActiveAdmin::SearchableSelect.inline_ajax_options = (example.metadata[:type] == :feature)
end
end
```
### Passing options to Select2
It is possible to pass and define configuration options to Select2
via `data-attributes` using nested (subkey) options.
Attributes need to be added to the `input_html` option in the form input.
For example you can tell Select2 how long to wait after a user
has stopped typing before sending the request:
```ruby
...
f.input(:category,
as: :searchable_select,
ajax: true,
input_html: {
data: {
'ajax--delay' => 500
}
})
...
```
## Development
To run the tests install bundled gems and invoke RSpec:
```
$ bundle
$ bundle exec rspec
```
The test suite can be run against different versions of Rails and
Active Admin (see `Appraisals` file):
```
$ appraisal install
$ appraisal rspec
```
Please make sure changes conform with the styleguide:
```
$ bundle exec rubocop
```
## Acknowledgements
Based on
[mfairburn/activeadmin-select2](https://github.com/mfairburn/activeadmin-select2).