#7123: Handle empty_option on API Select

This commit is contained in:
thatmattlove 2021-09-01 17:02:43 -07:00
parent 774dff07ee
commit ddff193786
4 changed files with 26 additions and 17 deletions

Binary file not shown.

Binary file not shown.

View File

@ -22,13 +22,6 @@ import type { Stringifiable } from 'query-string';
import type { Option } from 'slim-select/dist/data'; import type { Option } from 'slim-select/dist/data';
import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types'; import type { Trigger, PathFilter, ApplyMethod, QueryFilter } from './types';
// Empty placeholder option.
const PLACEHOLDER = {
value: '',
text: '',
placeholder: true,
} as Option;
// Attributes which if truthy should render the option disabled. // Attributes which if truthy should render the option disabled.
const DISABLED_ATTRIBUTES = ['occupied'] as string[]; const DISABLED_ATTRIBUTES = ['occupied'] as string[];
@ -52,6 +45,8 @@ export class APISelect {
*/ */
public readonly placeholder: string; public readonly placeholder: string;
public readonly emptyOption: Nullable<Option> = null;
/** /**
* Event that will initiate the API call to NetBox to load option data. By default, the trigger * Event that will initiate the API call to NetBox to load option data. By default, the trigger
* is `'load'`, so data will be fetched when the element renders on the page. * is `'load'`, so data will be fetched when the element renders on the page.
@ -147,7 +142,7 @@ export class APISelect {
/** /**
* This instance's available options. * This instance's available options.
*/ */
private _options: Option[] = [PLACEHOLDER]; private _options: Option[] = [];
/** /**
* Array of options values which should be considered disabled or static. * Array of options values which should be considered disabled or static.
@ -168,6 +163,14 @@ export class APISelect {
this.preSorted = true; this.preSorted = true;
} }
const emptyOption = base.getAttribute('data-empty-option');
if (isTruthy(emptyOption)) {
this.emptyOption = {
text: emptyOption,
value: '',
};
}
if (hasUrl(base)) { if (hasUrl(base)) {
const url = base.getAttribute('data-url') as string; const url = base.getAttribute('data-url') as string;
this.url = url; this.url = url;
@ -285,17 +288,16 @@ export class APISelect {
// Get the placeholder index (note: if there is no placeholder, the index will be `-1`). // Get the placeholder index (note: if there is no placeholder, the index will be `-1`).
const placeholderIdx = deduplicated.findIndex(o => o.value === ''); const placeholderIdx = deduplicated.findIndex(o => o.value === '');
if (hasPlaceholder && placeholderIdx < 0) { if (hasPlaceholder && placeholderIdx < 0 && this.emptyOption !== null) {
// If there is a placeholder but it is not the first element (due to sorting or other merge // If there is a placeholder but it is not the first element (due to sorting or other merge
// issues), remove it from the options array and place it in front. // issues), remove it from the options array and place it in front.
deduplicated.splice(placeholderIdx); deduplicated.splice(placeholderIdx);
deduplicated = [PLACEHOLDER, ...deduplicated]; deduplicated = [this.emptyOption, ...deduplicated];
} }
if (!hasPlaceholder) { if (!hasPlaceholder && this.emptyOption !== null) {
// If there is no placeholder, add one to the front of the array. // If there is no placeholder, add one to the front of the array.
deduplicated = [PLACEHOLDER, ...deduplicated]; deduplicated = [this.emptyOption, ...deduplicated];
} }
this._options = deduplicated; this._options = deduplicated;
this.slim.setData(deduplicated); this.slim.setData(deduplicated);
} }
@ -304,7 +306,11 @@ export class APISelect {
* Remove all options and reset back to the generic placeholder. * Remove all options and reset back to the generic placeholder.
*/ */
private resetOptions(): void { private resetOptions(): void {
this.options = [PLACEHOLDER]; if (this.emptyOption !== null) {
this.options = [this.emptyOption];
} else {
this.options = [];
}
} }
/** /**
@ -534,7 +540,7 @@ export class APISelect {
*/ */
private async getOptions(action: ApplyMethod = 'merge'): Promise<void> { private async getOptions(action: ApplyMethod = 'merge'): Promise<void> {
if (this.queryUrl.includes(`{{`)) { if (this.queryUrl.includes(`{{`)) {
this.options = [PLACEHOLDER]; this.resetOptions();
return; return;
} }
await this.fetchOptions(this.queryUrl, action); await this.fetchOptions(this.queryUrl, action);

View File

@ -376,7 +376,7 @@ class DynamicModelChoiceMixin:
widget = widgets.APISelect widget = widgets.APISelect
def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None, def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None,
*args, **kwargs): empty_label=None, *args, **kwargs):
self.query_params = query_params or {} self.query_params = query_params or {}
self.initial_params = initial_params or {} self.initial_params = initial_params or {}
self.null_option = null_option self.null_option = null_option
@ -386,11 +386,14 @@ class DynamicModelChoiceMixin:
# to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
# by widget_attrs() # by widget_attrs()
self.to_field_name = kwargs.get('to_field_name') self.to_field_name = kwargs.get('to_field_name')
self.empty_option = empty_label or ""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def widget_attrs(self, widget): def widget_attrs(self, widget):
attrs = {} attrs = {
'data-empty-option': self.empty_option
}
# Set value-field attribute if the field specifies to_field_name # Set value-field attribute if the field specifies to_field_name
if self.to_field_name: if self.to_field_name: