Module: Layered::Ui::ComboboxOptions

Defined in:
app/controllers/concerns/layered/ui/combobox_options.rb

Overview

Builds the JSON a combobox rendered with url: expects, so a remote option list is a couple of lines in the host app's controller rather than a hand-rolled endpoint:

class UsersController < ApplicationController
include Layered::Ui::ComboboxOptions

def options
  render json: l_ui_combobox_options(policy_scope(User), label: :name, search: [:name, :email])
end
end

The endpoint is deliberately plain: it reads term and page from the query string and answers with

{ "options": [{ "label": "Ada Lovelace", "value": "7" }], "page": 1, "pages": 3, "count": 42 }

Ransack and Pagy are used when they are available - Ransack to build the search predicate (so association attributes such as :author_name work), Pagy to take the page - and there is a plain Active Record fallback for when they are not, so neither gem is required.

Endpoints stay the host app's own actions on purpose: there is no generic "query any model" route to authorise, so the scope passed in is whatever the current user is allowed to see.

Constant Summary collapse

DEFAULT_LIMIT =
20
LIKE_PREDICATES =

The predicates the Ransack-less fallback can build for itself, mapping a quoted column onto its condition. Ransack has many more, so the rest are an error rather than a query quietly matching on something else. i_cont lowers both sides, as Ransack's own does, rather than reaching for an ILIKE only PostgreSQL has.

{
  cont: ->(column) { "#{column} LIKE :term" },
  i_cont: ->(column) { "LOWER(#{column}) LIKE :term" }
}.freeze

Instance Method Summary collapse

Instance Method Details

#l_ui_combobox_options(scope, label:, value: :id, search: nil, predicate: :cont, combinator: :or, term: nil, page: nil, limit: DEFAULT_LIMIT) ⇒ Object

Options:

label:      (Symbol/Proc) Attribute, or callable taking the record, used for the option's label.
value:      (Symbol/Proc) Attribute, or callable, used for the option's value. Defaults to +:id+.
search:     (Array)       Attributes the term is matched against. Defaults to +label+, so it is
                       required when +label+ is a callable.
predicate:  (Symbol)      Ransack predicate for the match (default +:cont+; +:i_cont+ ignores case).
                       Without Ransack only +:cont+ and +:i_cont+ can be built; any other raises.
combinator: (Symbol)      How the search attributes combine, +:or+ (default) or +:and+.
term:       (String)      The term to search for. Defaults to +params[:term]+.
page:       (Integer)     The page to return. Defaults to +params[:page]+.
limit:      (Integer)     Options per page (default 20).


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'app/controllers/concerns/layered/ui/combobox_options.rb', line 52

def l_ui_combobox_options(scope, label:, value: :id, search: nil, predicate: :cont,
                          combinator: :or, term: nil, page: nil, limit: DEFAULT_LIMIT)
  # Both come off the query string, where a parameter can arrive as any
  # shape Rack can parse - +?page[]=1+ hands over an Array - so each is
  # read through to_s. A page that makes no sense is the first page, as a
  # missing one is, rather than an exception from the coercion.
  term = (term.nil? ? l_ui_combobox_request_param(:term) : term).to_s.strip
  page = (page.nil? ? l_ui_combobox_request_param(:page) : page).to_s.to_i
  page = 1 if page < 1

  matches = term.empty? ? scope.all : l_ui_combobox_matches(scope, search || [label], term, predicate, combinator)
  matches = l_ui_combobox_ordered(matches, label)
  records, count, pages = l_ui_combobox_page(matches, page, limit)

  {
    options: records.map do |record|
      { label: l_ui_combobox_attribute(record, label).to_s,
        value: l_ui_combobox_attribute(record, value).to_s }
    end,
    page: page,
    pages: pages,
    count: count
  }
end