Skip to content

Commit

Permalink
Refactor <FetchSelect />
Browse files Browse the repository at this point in the history
  • Loading branch information
roncodes committed Oct 26, 2023
1 parent a32a1d5 commit 8163924
Show file tree
Hide file tree
Showing 10 changed files with 1,694 additions and 1,445 deletions.
18 changes: 14 additions & 4 deletions addon/components/fetch-select.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
<div class="fetch-select {{@wrapperClass}}" {{did-insert this.fetchOptions}}>
<Select ...attributes @fetched={{true}} @options={{this.options}} @placeholder={{this.placeholder}} @optionLabel={{@optionLabel}} @optionValue={{@optionValue}} @onSelect={{@onSelect}} @humanize={{@humanize}} as |option key optionLabel|>
{{yield option key optionLabel}}
</Select>
<div class="fleetbase-model-select fleetbase-power-select ember-model-select {{@wrapperClass}}">
<PowerSelect @afterOptionsComponent={{@afterOptionsComponent}} @allowClear={{@allowClear}} @animationEnabled={{@animationEnabled}} @ariaDescribedBy={{@ariaDescribedBy}} @ariaInvalid={{@ariaInvalid}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{@ariaLabelledBy}} @beforeOptionsComponent={{@beforeOptionsComponent}} @buildSelection={{@buildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @defaultHighlighted={{@defaultHighlighted}} @destination={{@destination}} @disabled={{@disabled}} @dropdownClass={{or @dropdownClass "ember-model-select__dropdown"}} @extra={{@extra}} @groupComponent={{@groupComponent}} @highlightOnHover={{@highlightOnHover}} @horizontalPosition={{@horizontalPosition}} @initiallyOpened={{@initiallyOpened}} @loadingMessage={{@loadingMessage}} @eventType={{@eventType}} @matcher={{@matcher}} @matchTriggerWidth={{@matchTriggerWidth}} @noMatchesMessage={{@noMatchesMessage}} @onBlur={{@onBlur}} @onChange={{this.onChange}} @onClose={{this.onClose}} @onFocus={{@onFocus}} @onInput={{this.onInput}} @onKeydown={{@onKeydown}} @onOpen={{this.onOpen}} @options={{this.options}} @optionsComponent={{component this.optionsComponent}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @renderInPlace={{@renderInPlace}} @scrollTo={{@scrollTo}} @search={{perform this.searchOptions}} @searchEnabled={{get-default-value @searchEnabled true}} @searchField={{@searchField}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} @selected={{this.selected}} @selectedItemComponent={{@selectedItemComponent}} @tabindex={{@tabindex}} @triggerClass="form-select form-input {{@triggerClass}}" @triggerComponent={{@triggerComponent}} @triggerId={{@triggerId}} @triggerRole={{@triggerRole}} @typeAheadMatcher={{@typeAheadMatcher}} @verticalPosition={{@verticalPosition}} @withCreate={{@withCreate}} ...attributes as |option|>
{{#if (has-block)}}
{{yield option}}
{{else}}
{{get option @optionLabel}}
{{/if}}
</PowerSelect>

{{#if this.fetchOptions.isRunning}}
<div class="ember-model-select__loading">
<ModelSelect::Spinner />
</div>
{{/if}}
</div>
235 changes: 213 additions & 22 deletions addon/components/fetch-select.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,233 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { isBlank } from '@ember/utils';
import { action, computed } from '@ember/object';
import { isEmpty } from '@ember/utils';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { assign } from '@ember/polyfills';
import { assert } from '@ember/debug';
import { timeout } from 'ember-concurrency';
import { restartableTask, dropTask } from 'ember-concurrency-decorators';

Check failure on line 10 in addon/components/fetch-select.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'dropTask' is defined but never used

/**
* FetchSelectComponent is a Glimmer component responsible for rendering a
* select input and fetching options asynchronously based on user input.
*
* @class FetchSelectComponent
* @extends Component
* @memberof FleetbaseComponents
*
* @property {Service} fetch - The fetch service injected into the component.
* @property {Array} options - The list of selectable options.
* @property {Object} selected - The currently selected option.
* @property {number} debounceDuration - The duration to debounce the search input, in milliseconds.
*/
export default class FetchSelectComponent extends Component {
/**
* The fetch service is used to make network requests to fetch the options for the select input.
* @type {Service}
*/
@service fetch;

/**
* The list of selectable options.
* @type {Array}
*/
@tracked options = [];
@tracked isLoading = true;

@computed('args.placeholder', 'isLoading') get palceholder() {
const { placeholder } = this.args;
/**
* The currently selected option.
* @type {Object}
*/
@tracked selected;

/**
* The duration to debounce the search input, in milliseconds.
* @type {number}
*/
@tracked debounceDuration = 250;

/**
* The constructor ensures that the endpoint argument is specified, and
* initializes the component's properties based on the arguments passed to it.
*/
constructor() {
super(...arguments);

assert('<FetchSelect /> requires a valid `endpoint`.', !isEmpty(this.args.endpoint));

this.endpoint = this.args.endpoint;
this.selected = this.setSelectedOption(this.args.selected);
// this.debounceDuration = this.args.debounceDuration || this.debounceDuration;
}

/**
* Searches for options based on the term provided. Debounces the search
* if it's not the initial load.
*
* @param {string} term - The search term.
* @param {Object} [options={}] - Additional options for the search.
* @param {boolean} [initialLoad=false] - Whether this is the initial load.
* @task
*/
@restartableTask({ withTestWaiter: true }) searchOptions = function* (term, options = {}, initialLoad = false) {

Check failure on line 73 in addon/components/fetch-select.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'options' is assigned a value but never used
if (!initialLoad) {
yield timeout(this.debounceDuration);
}

yield this.fetchOptions.perform(term, createOption);

Check failure on line 78 in addon/components/fetch-select.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'createOption' is not defined
};

if (placeholder) {
return placeholder;
/**
* Fetches options based on the term provided.
*
* @param {string} term - The search term.
* @param {Object} [options={}] - Additional options for the fetch.
* @task
*/
@restartableTask({ withTestWaiter: true }) fetchOptions = function* (term, options = {}) {
// query might be an EmptyObject/{{hash}}, make it a normal Object
const query = assign({}, this.args.query);
const endpoint = this.endpoint;

Check failure on line 91 in addon/components/fetch-select.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'endpoint' is assigned a value but never used

if (term) {
set(query, 'query', term);

Check failure on line 94 in addon/components/fetch-select.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'set' is not defined
}

if (this.isLoading) {
return 'Loading options...';
let _options = yield this.fetch.get(this.endpoint, query, options);

// if options returns is an object and not array
if (this.isFetchResponseObject(_options)) {
_options = this.convertOptionsObjectToArray(_options);
}

return null;
// set options
this.options = _options;
return _options;
};

convertOptionsObjectToArray(_options) {
const objectKeys = Object.keys(_options);
const _optionsFromObject = [];

objectKeys.forEach((key) => {
_optionsFromObject.pushObject({
key,
value: _options[key],
});
});

return _optionsFromObject;
}

@action fetchOptions() {
const { path } = this.args;
isFetchResponseObject(_options) {
return !isArray(_options) && typeof _options === 'object' && Object.keys(_options).length;
}

if (isBlank(path)) {
return;
}
/**
* Set the selected option.
*
* @param {*} selected
* @memberof FetchSelectComponent
*/
setSelectedOption(selected) {
const { optionValue } = this.args;

if (optionValue) {
this.fetchOptions.perform().then((options) => {
let foundSelected = null;

this.fetch
.get(path)
.then((options) => {
this.options = options;
})
.finally(() => {
this.isLoading = false;
if (isArray(options)) {
foundSelected = options.find((option) => option[optionValue] === selected);
}

if (foundSelected) {
this.selected = foundSelected;
} else {
this.selected = selected;
}
});
} else {
this.selected = selected;
}
}

/**
* Loads the default set of options.
*/
loadDefaultOptions() {
const { loadDefaultOptions } = this.args;

if (loadDefaultOptions === undefined || loadDefaultOptions) {
this.fetchOptions.perform(null, {}, true);
}
}

/**
* Called when the select input is opened.
* @action
*/
@action onOpen() {
const { onOpen } = this.args;

this.loadDefaultOptions();

if (typeof onOpen === 'function') {
onOpen(...arguments);
}
}

/**
* Called when the user inputs a search term.
*
* @param {string} term - The search term.
* @action
*/
@action onInput(term) {
const { onInput } = this.args;

if (isEmpty(term)) {
this.loadDefaultOptions();
}

if (typeof onInput === 'function') {
onInput(...arguments);
}
}

/**
* Called when an option is selected.
*
* @param {Object} option - The selected option.
* @action
*/
@action onChange(option, ...rest) {
const { onChange, optionValue } = this.args;

// set selected
this.selected = option;

// if option value supplied
if (optionValue && typeof option === 'object') {
option = option[optionValue];
}

if (typeof onChange === 'function') {
onChange(option, ...rest);
}
}

/**
* Called when the select input is closed.
* @action
*/
@action onClose() {
const { onClose } = this.args;

this.fetchOptions.cancelAll();

if (typeof onClose === 'function') {
onClose(...arguments);
}
}
}
2 changes: 1 addition & 1 deletion addon/components/filter/model.hbs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<ModelSelect @modelName={{@filter.model}} @query={{@filter.query}} @labelProperty={{or @filter.modelNamePath "name"}} @selectedModel={{this.selectedModel}} @placeholder={{@placeholder}} @triggerClass="form-select form-input form-input-sm flex-1" @infiniteScroll={{false}} @renderInPlace={{true}} @onChange={{this.onChange}} @allowClear={{true}} @onClear={{this.clear}} />
<ModelSelect @modelName={{@filter.model}} @query={{@filter.query}} @optionLabel={{or @filter.modelNamePath "name"}} @selectedModel={{this.selectedModel}} @placeholder={{@placeholder}} @triggerClass="form-select form-input form-input-sm flex-1" @infiniteScroll={{false}} @renderInPlace={{true}} @onChange={{this.onChange}} @allowClear={{true}} @onClear={{this.clear}} />
4 changes: 2 additions & 2 deletions addon/components/model-select-multiple.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<ModelSelect @modelName={{@modelName}} @selectedModel={{@selectedModel}} @labelProperty={{@labelProperty}} @searchProperty={{@searchProperty}} @searchKey={{@searchKey}} @loadDefaultOptions={{@loadDefaultOptions}} @infiniteScroll={{@infiniteScroll}} @pageSize={{@pageSize}} @query={{@query}} @debounceDuration={{this.debounceDuration}} @withCreate={{@withCreate}} @buildSuggestion={{@buildSuggestion}} @perPageParam={{@perPageParam}} @pageParam={{@pageParam}} @totalPagesParam={{@totalPagesParam}} @onCreate={{@onCreate}} {{!-- overwritten arguments --}} @onChange={{this.change}} @searchField={{@labelProperty}} @triggerClass="ember-model-select-multiple-trigger {{@triggerClass}}" {{!-- power-select-multiple defaults --}} @triggerRole={{@triggerRole}} @ariaDescribedBy={{@ariaDescribedBy}} @ariaInvalid={{@ariaInvalid}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{@ariaLabelledBy}} @afterOptionsComponent={{@afterOptionsComponent}} @allowClear={{@allowClear}} @beforeOptionsComponent={{or @beforeOptionsComponent null}} @buildSelection={{or @buildSelection this.defaultBuildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @defaultHighlighted={{@defaultHighlighted}} @destination={{@destination}} @disabled={{@disabled}} @dropdownClass={{@dropdownClass}} @extra={{@extra}} @groupComponent={{@groupComponent}} @horizontalPosition={{@horizontalPosition}} @initiallyOpened={{@initiallyOpened}} @loadingMessage={{@loadingMessage}} @matcher={{@matcher}} @matchTriggerWidth={{@matchTriggerWidth}} @noMatchesMessage={{@noMatchesMessage}} @onBlur={{@onBlur}} {{!-- @onChange={{@onChange}} --}} @onClose={{@onClose}} @onFocus={{this.handleFocus}} @onInput={{@onInput}} @onKeydown={{this.handleKeydown}} @onOpen={{this.handleOpen}} @options={{@options}} @optionsComponent={{@optionsComponent}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @registerAPI={{@registerAPI}} @renderInPlace={{@renderInPlace}} @required={{@required}} @scrollTo={{@scrollTo}} @search={{@search}} @searchEnabled={{@searchEnabled}} {{!-- @searchField={{@searchField}} --}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} {{!-- @selected={{@selected}} --}} @selectedItemComponent={{@selectedItemComponent}} @eventType={{@eventType}} @title={{@title}} {{!-- @triggerClass="ember-power-select-multiple-trigger {{@triggerClass}}" --}} @triggerComponent={{component (or @triggerComponent "power-select-multiple/trigger") tabindex=@tabindex}} @triggerId={{@triggerId}} @verticalPosition={{@verticalPosition}} @tabindex={{this.computedTabIndex}} ...attributes as |model|>
<ModelSelect @modelName={{@modelName}} @selectedModel={{@selectedModel}} @optionLabel={{@optionLabel}} @searchProperty={{@searchProperty}} @searchKey={{@searchKey}} @loadDefaultOptions={{@loadDefaultOptions}} @infiniteScroll={{@infiniteScroll}} @pageSize={{@pageSize}} @query={{@query}} @debounceDuration={{this.debounceDuration}} @withCreate={{@withCreate}} @buildSuggestion={{@buildSuggestion}} @perPageParam={{@perPageParam}} @pageParam={{@pageParam}} @totalPagesParam={{@totalPagesParam}} @onCreate={{@onCreate}} {{!-- overwritten arguments --}} @onChange={{this.change}} @searchField={{@optionLabel}} @triggerClass="ember-model-select-multiple-trigger {{@triggerClass}}" {{!-- power-select-multiple defaults --}} @triggerRole={{@triggerRole}} @ariaDescribedBy={{@ariaDescribedBy}} @ariaInvalid={{@ariaInvalid}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{@ariaLabelledBy}} @afterOptionsComponent={{@afterOptionsComponent}} @allowClear={{@allowClear}} @beforeOptionsComponent={{or @beforeOptionsComponent null}} @buildSelection={{or @buildSelection this.defaultBuildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @defaultHighlighted={{@defaultHighlighted}} @destination={{@destination}} @disabled={{@disabled}} @dropdownClass={{@dropdownClass}} @extra={{@extra}} @groupComponent={{@groupComponent}} @horizontalPosition={{@horizontalPosition}} @initiallyOpened={{@initiallyOpened}} @loadingMessage={{@loadingMessage}} @matcher={{@matcher}} @matchTriggerWidth={{@matchTriggerWidth}} @noMatchesMessage={{@noMatchesMessage}} @onBlur={{@onBlur}} {{!-- @onChange={{@onChange}} --}} @onClose={{@onClose}} @onFocus={{this.handleFocus}} @onInput={{@onInput}} @onKeydown={{this.handleKeydown}} @onOpen={{this.handleOpen}} @options={{@options}} @optionsComponent={{@optionsComponent}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @registerAPI={{@registerAPI}} @renderInPlace={{@renderInPlace}} @required={{@required}} @scrollTo={{@scrollTo}} @search={{@search}} @searchEnabled={{@searchEnabled}} {{!-- @searchField={{@searchField}} --}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} {{!-- @selected={{@selected}} --}} @selectedItemComponent={{@selectedItemComponent}} @eventType={{@eventType}} @title={{@title}} {{!-- @triggerClass="ember-power-select-multiple-trigger {{@triggerClass}}" --}} @triggerComponent={{component (or @triggerComponent "power-select-multiple/trigger") tabindex=@tabindex}} @triggerId={{@triggerId}} @verticalPosition={{@verticalPosition}} @tabindex={{this.computedTabIndex}} ...attributes as |model|>
{{#if (has-block)}}
{{yield model}}
{{else}}
{{get model @labelProperty}}
{{get model @optionLabel}}
{{/if}}
</ModelSelect>
6 changes: 3 additions & 3 deletions addon/components/model-select.hbs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
<div class="fleetbase-model-select fleetbase-power-select ember-model-select {{@wrapperClass}}">
<PowerSelect @afterOptionsComponent={{@afterOptionsComponent}} @allowClear={{@allowClear}} @animationEnabled={{@animationEnabled}} @ariaDescribedBy={{@ariaDescribedBy}} @ariaInvalid={{@ariaInvalid}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{@ariaLabelledBy}} @beforeOptionsComponent={{@beforeOptionsComponent}} @buildSelection={{@buildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @defaultHighlighted={{@defaultHighlighted}} @destination={{@destination}} @disabled={{@disabled}} @dropdownClass={{or @dropdownClass "ember-model-select__dropdown"}} @extra={{@extra}} @groupComponent={{@groupComponent}} @highlightOnHover={{@highlightOnHover}} @horizontalPosition={{@horizontalPosition}} @initiallyOpened={{@initiallyOpened}} @loadingMessage={{@loadingMessage}} @eventType={{@eventType}} @matcher={{@matcher}} @matchTriggerWidth={{@matchTriggerWidth}} @noMatchesMessage={{@noMatchesMessage}} @onBlur={{@onBlur}} @onChange={{this.change}} @onClose={{this.onClose}} @onFocus={{@onFocus}} @onInput={{this.onInput}} @onKeydown={{@onKeydown}} @onOpen={{this.onOpen}} @options={{this._options}} @optionsComponent={{component
this.optionsComponent
infiniteScroll=this.infiniteScroll
infiniteModel=this.model
withCreate=this.withCreate}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @renderInPlace={{@renderInPlace}} @scrollTo={{@scrollTo}} @search={{perform this.searchModels}} @searchEnabled={{get-default-value @searchEnabled true}} @searchField={{@searchField}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} @selected={{this.selectedModel}} @selectedItemComponent={{@selectedItemComponent}} @tabindex={{@tabindex}} @triggerClass={{@triggerClass}} @triggerComponent={{@triggerComponent}} @triggerId={{@triggerId}} @triggerRole={{@triggerRole}} @typeAheadMatcher={{@typeAheadMatcher}} @verticalPosition={{@verticalPosition}} @withCreate={{@withCreate}} ...attributes as |model|>
withCreate=this.withCreate}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @renderInPlace={{@renderInPlace}} @scrollTo={{@scrollTo}} @search={{perform this.searchModels}} @searchEnabled={{get-default-value @searchEnabled true}} @searchField={{@searchField}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} @selected={{this.selectedModel}} @selectedItemComponent={{@selectedItemComponent}} @tabindex={{@tabindex}} @triggerClass="form-select form-input {{@triggerClass}}" @triggerComponent={{@triggerComponent}} @triggerId={{@triggerId}} @triggerRole={{@triggerRole}} @typeAheadMatcher={{@typeAheadMatcher}} @verticalPosition={{@verticalPosition}} @withCreate={{@withCreate}} ...attributes as |model|>
{{#if (has-block)}}
{{yield model}}
{{else}}
{{get model @labelProperty}}
{{get model @optionLabel}}
{{/if}}
</PowerSelect>

Expand Down
Loading

0 comments on commit 8163924

Please sign in to comment.