active_element

0.0.13
Light Mode Dark Mode

Text Search

ActiveElement provides a text_search_field capable of generating a full text search auto-suggest widget in a form.

The text_search_field accepts user input and presents a list of matching options, setting the field’s value to a value specified by the field’s configuration.

For example, you may want a user_id field that allows users to search by name and email on the User model, and returning the id for the selected result.

The field requires some extra configuration to prevent leaking unwanted data to the front end, and to allow you to select which columns should be searched and which column should be submitted to your controller params as the field’s value.

Example

We’ll make a form that creates a Pet record and associates it with a User by setting a user_id field.

Click the Rendered Output tab and type a few characters into the search field. In the real world this would be connected to your application, but for the purpose of this documentation we’ve stubbed the Javascript fetch function to return some fake results based on the input.

subject do
  active_element.component.form model: Pet.new,
                                fields: [
                                  :name,
                                  :animal,
                                  [:user_id,
                                   :text_search_field,
                                   { search: { model: :user, with: [:name, :email], providing: :id } }]
                                ]
end

it { is_expected.to include 'Search...' }
<div class="form pb-3" id="form-wrapper-active-element-0832a6a2-5aa7-42db-b036-7d213322353a">
  <form id="active-element-0832a6a2-5aa7-42db-b036-7d213322353a" class="pet m-3" action="/_pets" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="NS5dCFqsZlO_Uy3vKiRt3JaI_GeedbyJj9wGveQzcuTiKQLTmMZgy-zQvHb3KMuYAX3sCBJpJ593bYWTBlDN4A" autocomplete="off" />
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_name">
          Name
        </label>
      </div>
      <div class="col">
        <input class="form-control " tabindex="2" label="Name" type="text" name="pet[name]" id="pet_name" />
      </div>
    </div>
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_animal">
          Animal
        </label>
      </div>
      <div class="col">
        <input class="form-control " tabindex="3" label="Animal" type="text" name="pet[animal]" id="pet_animal" />
      </div>
    </div>
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_user_id">
          User ID
        </label>
      </div>
      <div class="col">
        <input id="active-element-0832a6a2-5aa7-42db-b036-7d213322353a-user_id-text-search" class="form-control " autocomplete="off" placeholder="Search..." tabindex="4" data-field-type="text-search" data-form-id="active-element-0832a6a2-5aa7-42db-b036-7d213322353a" data-search-attributes="[&quot;name&quot;,&quot;email&quot;]" data-search-value="id" data-search-model="user" type="text" name="pet[user_id]" />
        <input id="active-element-0832a6a2-5aa7-42db-b036-7d213322353a-user_id-text-search-hidden-value" autocomplete="off" type="hidden" name="pet[user_id]" />
      </div>
    </div>
    <div class="form-group">
      <input type="submit" name="" value="Create Pet" class="btn btn-success" data-disable-with="Create Pet" />
      <a data-form-input-type="clear" class="btn btn-primary  btn-secondary ms-2" title="Clear Form" href="#">
        <span class="text-nowrap">
          <span class="button-title">Clear Form</span>
        </span>
      </a>
    </div>
  </form>
</div>

When you select an option from the suggestions, the full display value appears in the field, but the controller only receives the id attribute in params[:pet][:user_id] - the display value is a hidden field that gets overwritten by the actual value.

Routes

If you run rails routes in your project once you’ve set up ActiveElement you’ll notice an extra route added for each of your controllers:

$ rails routes

Routes for ActiveElement::Engine:
pets__active_element_text_search POST /pets/_active_element_text_search(.:format)    pets#_active_element_text_search

These routes receive text queries and generate results based on your configuration. They’re required for the text_search_field to work but you can safely ignore them, they are protected by permissions if you have authorization configured, as well as some model configuration to ensure that only fields you explicitly configure are searchable.

Configuration

Model Configuration

Allowing users to search arbitrary columns on arbitrary models would be a major security risk. To prevent unauthorized data access and DoS vulnerabilities (e.g. allowing searching unindexed database columns), ActiveElement requires that models define which columns are searchable and which columns can provide values. The example above requires the following model definition in order to work:

# app/models/user.rb

class User < ApplicationRecord
  authorize_active_element_text_search with: [:name, :email], providing: :id
end

Now even if an attacker sends a custom request to the text search endpoint of your application, only the name and email columns on the users table are searchable, while the id column can be returned in results but not searched.

Important note: Specifying columns as searchable using the with keyword implicitly permits their matched values to be returned in the result sent back to the front end. This is for two reasons:

  1. Displaying the full matched search parameters provides a more coherent user experience. If we only return the id, the user won’t know if they’re selecting the option they’re looking for.
  2. Forcing the display of searched values is intended to reduce the risk of giving developers a false sense of security that only values listed in the providing keyword will be exposed. An attacker can use trivial techniques to gain the full value of a searchable column without being able to see it in the front end. By including the matched search values in the result, there is no ambiguity that these values are exposed to the front end application.

Inline Configuration

To allow re-use and to prevent cluttering your views, it is recommended to use file-based configuration, but inline configuration is also available.

We’ll re-use the example from above, breaking down exactly what’s happening:

subject do
  active_element.component.form model: Pet.new,
                                fields: [
                                  :name,
                                  :animal,
                                  [:user_id,
                                   :text_search_field,
                                   { search: { model: :user, with: [:name, :email], providing: :id } }]
                                ]
end

it { is_expected.to include 'Search...' }
<div class="form pb-3" id="form-wrapper-active-element-bae1f6c4-aea5-487c-8984-b6fc2b8d359f">
  <form id="active-element-bae1f6c4-aea5-487c-8984-b6fc2b8d359f" class="pet m-3" action="/_pets" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="YHSVrQ35dJEsswPelYSSek-wiailc77vCMTrYPTjigojNUa32Ap70N2ywOjczh4-gByztuVcf0VpIMZChQmRtQ" autocomplete="off" />
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_name">
          Name
        </label>
      </div>
      <div class="col">
        <input class="form-control " tabindex="2" label="Name" type="text" name="pet[name]" id="pet_name" />
      </div>
    </div>
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_animal">
          Animal
        </label>
      </div>
      <div class="col">
        <input class="form-control " tabindex="3" label="Animal" type="text" name="pet[animal]" id="pet_animal" />
      </div>
    </div>
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_user_id">
          User ID
        </label>
      </div>
      <div class="col">
        <input id="active-element-bae1f6c4-aea5-487c-8984-b6fc2b8d359f-user_id-text-search" class="form-control " autocomplete="off" placeholder="Search..." tabindex="4" data-field-type="text-search" data-form-id="active-element-bae1f6c4-aea5-487c-8984-b6fc2b8d359f" data-search-attributes="[&quot;name&quot;,&quot;email&quot;]" data-search-value="id" data-search-model="user" type="text" name="pet[user_id]" />
        <input id="active-element-bae1f6c4-aea5-487c-8984-b6fc2b8d359f-user_id-text-search-hidden-value" autocomplete="off" type="hidden" name="pet[user_id]" />
      </div>
    </div>
    <div class="form-group">
      <input type="submit" name="" value="Create Pet" class="btn btn-success" data-disable-with="Create Pet" />
      <a data-form-input-type="clear" class="btn btn-primary  btn-secondary ms-2" title="Clear Form" href="#">
        <span class="text-nowrap">
          <span class="button-title">Clear Form</span>
        </span>
      </a>
    </div>
  </form>
</div>

First we invoke the form component which renders our HTML form, specifying Pet.new as the model, just like a regular Rails form.

The fields array uses the usual format for the name and animal fields - the field type will be derived from the database type for each field.

The user_id field is specified as an array with three elements:

  1. The field name, user_id. This provides params[:pet][:user_id] when the form is submitted to the controller.
  2. The field type, ActiveElement’s text_search_field.
  3. An options hash, specifically including a search key that defines model, with, and providing.

It’s important to clarify that specifying these fields in the view is not sufficient to provide security, since they simply provide metadata to help the front-end Javascript component send the correct parameters. A user with access to the application could modify these fields and send arbitrary requests, which is why the model configuration is required.

The model option translates :user into User to identify the ActiveRecord model to use in the search, and the with and providing options specify the searchable fields (with) and the value field (providing). Only the providing field is included in the request params.

File-based Configuraton

Using inline configuration is fine for one-offs and examples, but it’s easy to imagine a form definition becoming cluttered with multiple search fields defined, as well as requiring duplicating and maintaining the same search fields across different forms (e.g. edit and new).

To mitigate this, file-based configuration is recommended for text search fields.

The above configuration can be redefined by creating config/forms/pet/user_id.yml:

# config/forms/pet/user_id.yml

---
type: text_search_field
options:
  search:
    model: user
    with:
    - name
    - email
    providing: id

You can still use inline configuration to override these settings, but now the form can be defined as:

subject do
  active_element.component.form model: Pet.new, fields: [:name, :animal, :user_id]
end

it { is_expected.to include 'Search...' }
<div class="form pb-3" id="form-wrapper-active-element-87910aa0-5a33-49b0-9ec6-2253c4e5da99">
  <form id="active-element-87910aa0-5a33-49b0-9ec6-2253c4e5da99" class="pet m-3" action="/_pets" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="BWdJE8l5M62s2EnitqhAr-d3BzatIlDPMOGsyp7UIui9pbNCMe6KMu6X_Ghxv0yteJvzNxeYMBr0sJnj72gR8w" autocomplete="off" />
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_name">
          Name
        </label>
      </div>
      <div class="col">
        <input class="form-control " tabindex="2" label="Name" type="text" name="pet[name]" id="pet_name" />
      </div>
    </div>
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_animal">
          Animal
        </label>
      </div>
      <div class="col">
        <input class="form-control " tabindex="3" label="Animal" type="text" name="pet[animal]" id="pet_animal" />
      </div>
    </div>
    <div class="row form-fields mb-3">
      <div class="col-sm-3">
        <label for="pet_user_id">
          User ID
        </label>
      </div>
      <div class="col">
        <input id="active-element-87910aa0-5a33-49b0-9ec6-2253c4e5da99-user_id-text-search" class="form-control " autocomplete="off" placeholder="Search..." tabindex="4" data-field-type="text-search" data-form-id="active-element-87910aa0-5a33-49b0-9ec6-2253c4e5da99" data-search-attributes="[&quot;name&quot;,&quot;email&quot;]" data-search-value="id" data-search-model="user" type="text" name="pet[user_id]" />
        <input id="active-element-87910aa0-5a33-49b0-9ec6-2253c4e5da99-user_id-text-search-hidden-value" autocomplete="off" type="hidden" name="pet[user_id]" />
      </div>
    </div>
    <div class="form-group">
      <input type="submit" name="" value="Create Pet" class="btn btn-success" data-disable-with="Create Pet" />
      <a data-form-input-type="clear" class="btn btn-primary  btn-secondary ms-2" title="Clear Form" href="#">
        <span class="text-nowrap">
          <span class="button-title">Clear Form</span>
        </span>
      </a>
    </div>
  </form>
</div>

Documentation generated by rspec-documentation