Autocomplete (APIv4 EntityReference) Fields¶
Overview¶
An Autocomplete is a dropdown field for selecting from a list of existing records. The query and results are handled by Search Kit. The widget is a Select2.
Note
As of this writing in 2023, CiviCRM is gradually migrating from the older v3-based EntityRef fields to v4-based Autocompletes. Afforms, custom fields of type Entity Reference, and the Search Kit UI already use this new widget, whereas other Quickform and Angular fields use the old one.
Adding an Autocomplete to a Form¶
- PHP (QuickForm): Use the
CRM_Core_Form::addAutocomplete()
method. - Javascript (jQuery): Start with an
<input>
element and call$('#element-selector).crmAutocomplete()
. - AngularJS: Use the
crmAutocomplete
directive on an<input>
element.
The APIv4 autocomplete
action¶
To populate its results, the form widget will call (e.g. for a Contact autocomplete):
CRM.api4('Contact', 'autocomplete', {
formName: 'x',
fieldName: 'y',
input: 'z',
page: 1,
filters: {contact_type: 'Individual'}
});
This autocomplete
API action in turn calls Search Kit's SearchDisplay::run
API to run the search
and output the results. Several events are fired which allow developers to modify the behavior, and they receive
contextual information like entityName, formName and fieldName to help listeners decide which events to respond to:
Modifying the Filters¶
By default, all filters passed to the autocomplete api will be silently discarded if those fields are not already part of the displayed output.
This is by design to keep the api secure. In the above example, passing the filter {contact_type: 'Individual'}
will work, because
contact_type
is already shown by default in autocomplete results. However most other fields would not work as filters
(e.g. external_identifier
, api_key
, gender_id
are not displayed as part of a contact's autocomplete results so would not be accepted as filters).
If you want non-displaying field to be accepted as fields from the client, or you wish to hard-code other filters in a particular autocomplete, add a callback for the civi.api.prepare
event. E.g.:
// Assuming your class implements HookInterface, AutoService, and your extension has the @scan-classes mixin:
public function on_civi_api_prepare(\Civi\API\Event\PrepareEvent $event): void {
$apiRequest = $event->getApiRequest();
if (
// APIv3 requests are not an object so check that first
is_object($apiRequest) &&
// We're only interested in Autocomplete actions
is_a($apiRequest, 'Civi\Api4\Generic\AutocompleteAction') &&
// Conventions for formName are "afform:afNameOfMyForm" for afforms and "qf:CRM_CLASS_OF_MY_FORM" for quickforms
$apiRequest->getFormName() === 'whatever-you-are-expecting-for-formName' &&
// Convention for fieldName is usually "entity:field" so e.g. "Participant:event_id"
$apiRequest->getFieldName() === 'whatever-you-are-expecting-for-fieldName' &&
// Or for less specificity you could respond to all autocompletes for a particular entity
$apiRequest->getEntityName() === 'Event'
) {
// Filters from the client request; will probably be discarded unless we intervene
$clientFilters = $apiRequest->getFilters();
// Allow filtering by campaign
if (isset($clientFilters['campaign_id'])) {
// The `AutocompleteAction::addFilter` method both sets the value and "whitelists" the filter to ensure it gets applied.
$apiRequest->addFilter('campaign_id', $clientFilters['campaign_id']);
}
}
}
Modifying the Display¶
If you wish to alter the SELECT, ORDER BY, LIMIT, or to change the formatting of the dropdown results, use this technique to alter the display.
Most entities rely on the default autocomplete display generated by DefaultDisplaySubscriber::autocompleteDefault
which returns label
, id
, icon
, color
, etc gathered from the entity's metadata. Other entities which do not have a label
or title
require a function to define a default display. For example, the ParticipantAutocompleteProvider
combines the name of the contact with the name of the event for a meaningful label.
Tip
At least one column is requred. The first is the primary text of the search results, and (if the column uses rewrite
)
all fields displayed by this column will be the fields searched on (combined with OR).
If you need to provide a default display for an entity, or you wish to override the display of an autocomplete, add a callback for the 'civi.search.defaultDisplay'
event. E.g.:
// Assuming your class implements HookInterface, AutoService, and your extension has the @scan-classes mixin:
public function on_civi_search_defaultDisplay(\Civi\Core\Event\GenericHookEvent $event): void {
if ($event->display['type'] === 'autocomplete' && $event->savedSearch['api_entity'] === 'Participant') {
$event->display['settings'] = [
'sort' => [...],
'columns' => [...],
'extra' => [...],
];
}
}
Display Fields will Auto-Select
When adding a field to the display (even as a token in e.g. a rewrite
) it will be auto-added to the
SELECT clause for you; no need to modify the query just to add a field.
Modifying the Query¶
If you wish to add WHERE, HAVING, or JOIN clauses to an autocomplete, use this technique (note that other query parameters are controlled by the Display, see above).
The query parameters for an Autocomplete are modeled as a Search Kit SavedSearch -- although in most cases the search isn't acually saved anywhere;
by default it starts with an empty array as the API parameters. SearchKit doesn't actually need any api params other than [version => 4]
to run a display,
since the display will add all of its fields to the SELECT and control the ORDER BY based on its 'sort'
param.
If you need to change the query in other ways, e.g. to JOIN to another entity, add a callback for the 'civi.search.autocompleteDefault'
event.
This event has access to formName
and fieldName
(added in CiviCRM 5.64) in case you want to do it for a specific field, or it can be per-entity like in the
CaseAutocompleteProvider
which adds a join on CaseContact
so the name of the client can be displayed (and searched on) with the case subject.
Here is an example from the Per-contact Note Access extension to list only contacts that have a CMS account:
// Assuming your class implements HookInterface, AutoService, and your extension has the @scan-classes mixin:
public function on_civi_search_autocompleteDefault(GenericHookEvent $event): void {
if (!is_array($event->savedSearch) || $event->savedSearch['api_entity'] !== 'Contact') {
return;
}
if ($event->formName === 'qf:CRM_Note_Form_Note' && $event->fieldName === 'noteaccess_contact_id') {
$event->savedSearch['api_params'] = [
'version' => 4,
'select' => [
'id',
'display_name',
'contact_type',
],
'orderBy' => [],
'where' => [],
'groupBy' => [],
'join' => [
[
'UFMatch AS uf_match',
'INNER',
['uf_match.contact_id', '=', 'id'],
],
],
'having' => [],
];
}
}