[MAJOR] Remove `.ajax` and allow async function for `.setChoices` (#701)

* WIP: remove ajax

* copy #700

* remove ajax, add fetchChoices

* extend setChoices

* update README
This commit is contained in:
Konstantin Vyatkin 2019-10-29 14:12:32 -04:00 committed by Josh Johnson
parent 1f5192b4ad
commit e3cc6eaf1b
9 changed files with 631 additions and 425 deletions

View File

@ -863,7 +863,9 @@ choices.disable();
**Input types affected:** `select-one`, `select-multiple`
**Usage:** Set choices of select input via an array of objects, a value name and a label name. This behaves the same as passing items via the `choices` option but can be called after initialising Choices. This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices. Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc). Passing an empty array as the first parameter, and a true `replaceChoices` is the same as calling `clearChoices` (see below).
**Usage:** Set choices of select input via an array of objects (or function that returns array of object or promise of it), a value field name and a label field name.
This behaves the similar as passing items via the `choices` option but can be called after initialising Choices. This can also be used to add groups of choices (see example 3); Optionally pass a true `replaceChoices` value to remove any existing choices. Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc). Passing an empty array as the first parameter, and a true `replaceChoices` is the same as calling `clearChoices` (see below).
**Example 1:**
@ -887,6 +889,22 @@ example.setChoices(
```js
const example = new Choices(element);
// Passing a function that returns Promise of choices
example.setChoices(async () => {
try {
const items = await fetch('/items');
return items.json();
} catch (err) {
console.error(err);
}
});
```
**Example 3:**
```js
const example = new Choices(element);
example.setChoices(
[
{
@ -1009,49 +1027,6 @@ example.setChoiceByValue('Two'); // Choice with value of 'Two' has now been sele
**Usage:** Enables input to accept new values/select further choices.
### ajax(fn);
**Input types affected:** `select-one`, `select-multiple`
**Usage:** Populate choices/groups via a callback.
**Example:**
```js
var example = new Choices(element);
example.ajax(function(callback) {
fetch(url)
.then(function(response) {
response.json().then(function(data) {
callback(data, 'value', 'label');
});
})
.catch(function(error) {
console.log(error);
});
});
```
**Example 2:**
If your structure differs from `data.value` and `data.key` structure you can write your own `key` and `value` into the `callback` function. This could be useful when you don't want to transform the given response.
```js
const example = new Choices(element);
example.ajax(function(callback) {
fetch(url)
.then(function(response) {
response.json().then(function(data) {
callback(data, 'data.key', 'data.value');
});
})
.catch(function(error) {
console.log(error);
});
});
```
## Browser compatibility
Choices is compiled using [Babel](https://babeljs.io/) to enable support for [ES5 browsers](http://caniuse.com/#feat=es5). If you need to support a browser that does not support one of the features listed below, I suggest including a polyfill from the very good [polyfill.io](https://cdn.polyfill.io/v2/docs/):

View File

@ -320,7 +320,7 @@
</select>
<label for="choices-single-remove-xhr"
>Options from remote source (XHR) &amp; remove button</label
>Options from remote source (Fetch API) &amp; remove button</label
>
<select
class="form-control"
@ -627,17 +627,17 @@
placeholder: true,
placeholderValue: 'Pick an Strokes record',
maxItemCount: 5,
}).ajax(function(callback) {
fetch(
}).setChoices(function() {
return fetch(
'https://api.discogs.com/artists/55980/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW',
)
.then(function(response) {
response.json().then(function(data) {
callback(data.releases, 'title', 'title');
});
return response.json();
})
.catch(function(error) {
console.error(error);
.then(function(data) {
return data.releases.map(function(release) {
return { value: release.title, label: release.title };
});
});
});
@ -685,46 +685,39 @@
var singleFetch = new Choices('#choices-single-remote-fetch', {
searchPlaceholderValue: 'Search for an Arctic Monkeys record',
}).ajax(function(callback) {
fetch(
'https://api.discogs.com/artists/391170/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW',
)
.then(function(response) {
response.json().then(function(data) {
callback(data.releases, 'title', 'title');
singleFetch.setChoiceByValue('Fake Tales Of San Francisco');
})
.setChoices(function() {
return fetch(
'https://api.discogs.com/artists/391170/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW',
)
.then(function(response) {
return response.json();
})
.then(function(data) {
return data.releases.map(function(release) {
return { label: release.title, value: release.title };
});
});
})
.catch(function(error) {
console.error(error);
});
});
})
.then(function(instance) {
instance.setChoiceByValue('Fake Tales Of San Francisco');
});
var singleXhrRemove = new Choices('#choices-single-remove-xhr', {
removeItemButton: true,
searchPlaceholderValue: "Search for a Smiths' record",
}).ajax(function(callback) {
var request = new XMLHttpRequest();
request.open(
'get',
}).setChoices(function(callback) {
return fetch(
'https://api.discogs.com/artists/83080/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW',
true,
);
request.onreadystatechange = function() {
var status;
var data;
if (request.readyState === 4) {
status = request.status;
if (status === 200) {
data = JSON.parse(request.responseText);
callback(data.releases, 'title', 'title');
singleXhrRemove.setChoiceByValue('How Soon Is Now?');
} else {
console.error(status);
}
}
};
request.send();
)
.then(function(res) {
return res.json();
})
.then(function(data) {
return data.releases.map(function(release) {
return { label: release.title, value: release.title };
});
});
});
var genericExamples = new Choices('[data-trigger]', {

View File

@ -1,28 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,user-scalable=no"
/>
<title>Choices</title>
<meta
name="description"
itemprop="description"
content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency."
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Choices</title>
<meta name=description itemprop=description content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" href="../assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="../assets/images/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="../assets/images/manifest.json">
<link rel="mask-icon" href="../assets/images/safari-pinned-tab.svg" color="#00bcd4">
<link rel="shortcut icon" href="../assets/images/favicon.ico">
<meta name="msapplication-config" content="../assets/images/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<!-- End ignore these -->
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3">
<!-- End ignore these -->
<!-- Choices includes -->
<link rel="stylesheet" href="../assets/styles/choices.min.css?version=6.0.3">
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>
<body>
@ -33,7 +63,12 @@
<label for="choices-basic">Basic</label>
<button class="disable push-bottom">Disable</button>
<button class="enable push-bottom">Enable</button>
<select class="form-control" name="choices-basic" id="choices-basic" multiple>
<select
class="form-control"
name="choices-basic"
id="choices-basic"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Find me">Choice 3</option>
@ -43,7 +78,12 @@
<div data-test-hook="remove-button">
<label for="choices-remove-button">Remove button</label>
<select class="form-control" name="choices-remove-button" id="choices-remove-button" multiple>
<select
class="form-control"
name="choices-remove-button"
id="choices-remove-button"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -53,7 +93,12 @@
<div data-test-hook="disabled-choice">
<label for="choices-disabled-choice">Disabled choice</label>
<select class="form-control" name="choices-disabled-choice" id="choices-disabled-choice" multiple>
<select
class="form-control"
name="choices-disabled-choice"
id="choices-disabled-choice"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -63,7 +108,12 @@
<div data-test-hook="add-items-disabled">
<label for="choices-add-items-disabled">Add items disabled</label>
<select class="form-control" name="choices-add-items-disabled" id="choices-add-items-disabled" multiple>
<select
class="form-control"
name="choices-add-items-disabled"
id="choices-add-items-disabled"
multiple
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -72,7 +122,13 @@
<div data-test-hook="disabled-via-attr">
<label for="choices-disabled-via-attr">Disabled via attribute</label>
<select class="form-control" name="choices-disabled-via-attr" id="choices-disabled-via-attr" multiple disabled>
<select
class="form-control"
name="choices-disabled-via-attr"
id="choices-disabled-via-attr"
multiple
disabled
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -81,7 +137,12 @@
<div data-test-hook="selection-limit">
<label for="choices-selection-limit">Input limit</label>
<select class="form-control" name="choices-selection-limit" id="choices-selection-limit" multiple>
<select
class="form-control"
name="choices-selection-limit"
id="choices-selection-limit"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -93,7 +154,12 @@
<div data-test-hook="prepend-append">
<label for="choices-prepend-append">Prepend/append</label>
<select class="form-control" name="choices-prepend-append" id="choices-prepend-append" multiple>
<select
class="form-control"
name="choices-prepend-append"
id="choices-prepend-append"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -102,7 +168,12 @@
<div data-test-hook="render-choice-limit">
<label for="choices-render-choice-limit">Render choice limit</label>
<select class="form-control" name="choices-render-choice-limit" id="choices-render-choice-limit" multiple>
<select
class="form-control"
name="choices-render-choice-limit"
id="choices-render-choice-limit"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -111,7 +182,12 @@
<div data-test-hook="search-floor">
<label for="choices-search-floor">Search floor</label>
<select class="form-control" name="choices-search-floor" id="choices-search-floor" multiple>
<select
class="form-control"
name="choices-search-floor"
id="choices-search-floor"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -120,7 +196,12 @@
<div data-test-hook="placeholder">
<label for="choices-placeholder">Placeholder</label>
<select class="form-control" name="choices-placeholder" id="choices-placeholder" multiple>
<select
class="form-control"
name="choices-placeholder"
id="choices-placeholder"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -129,12 +210,22 @@
<div data-test-hook="remote-data">
<label for="choices-remote-data">Remote data</label>
<select class="form-control" name="choices-remote-data" id="choices-remote-data" multiple></select>
<select
class="form-control"
name="choices-remote-data"
id="choices-remote-data"
multiple
></select>
</div>
<div data-test-hook="scrolling-dropdown">
<label for="choices-scrolling-dropdown">Scrolling dropdown</label>
<select class="form-control" name="choices-scrolling-dropdown" id="choices-scrolling-dropdown" multiple>
<select
class="form-control"
name="choices-scrolling-dropdown"
id="choices-scrolling-dropdown"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -155,7 +246,12 @@
<div data-test-hook="groups">
<label for="choices-groups">Choice groups</label>
<select class="form-control" name="choices-groups" id="choices-groups" multiple>
<select
class="form-control"
name="choices-groups"
id="choices-groups"
multiple
>
<optgroup label="UK">
<option value="London">London</option>
<option value="Manchester">Manchester</option>
@ -171,26 +267,47 @@
<div data-test-hook="custom-properties">
<label for="choices-custom-properties">Custom properties</label>
<select class="form-control" name="choices-custom-properties" id="choices-custom-properties" multiple></select>
<select
class="form-control"
name="choices-custom-properties"
id="choices-custom-properties"
multiple
></select>
</div>
<div data-test-hook="non-string-values">
<label for="choices-non-string-values">Non-string values</label>
<select class="form-control" name="choices-non-string-values" id="choices-non-string-values"></select>
<select
class="form-control"
name="choices-non-string-values"
id="choices-non-string-values"
></select>
</div>
<div data-test-hook="within-form">
<form>
<label for="choices-within-form">Within form</label>
<select class="form-control" name="choices-within-form" id="choices-within-form" multiple>
<select
class="form-control"
name="choices-within-form"
id="choices-within-form"
multiple
>
<option value="Choice 1">Choice 1</option>
</select>
</form>
</div>
<div data-test-hook="set-choice-by-value">
<label for="choices-set-choice-by-value">Dynamically set choice by value</label>
<select class="form-control" name="choices-set-choice-by-value" id="choices-set-choice-by-value" multiple>
<label for="choices-set-choice-by-value"
>Dynamically set choice by value</label
>
<select
class="form-control"
name="choices-set-choice-by-value"
id="choices-set-choice-by-value"
multiple
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -199,7 +316,12 @@
<div data-test-hook="search-by-label">
<label for="choices-search-by-label">Search by label</label>
<select class="form-control" name="choices-search-by-label" id="choices-search-by-label" multiple>
<select
class="form-control"
name="choices-search-by-label"
id="choices-search-by-label"
multiple
>
<option value="value1">label1</option>
<option value="value2">label2</option>
</select>
@ -210,13 +332,17 @@
document.addEventListener('DOMContentLoaded', function() {
const choicesBasic = new Choices('#choices-basic');
document.querySelector('button.disable').addEventListener('click', () => {
choicesBasic.disable();
});
document
.querySelector('button.disable')
.addEventListener('click', () => {
choicesBasic.disable();
});
document.querySelector('button.enable').addEventListener('click', () => {
choicesBasic.enable();
});
document
.querySelector('button.enable')
.addEventListener('click', () => {
choicesBasic.enable();
});
new Choices('#choices-remove-button', {
removeItemButton: true,
@ -254,16 +380,9 @@
new Choices('#choices-remote-data', {
shouldSort: false,
}).ajax((callback) => {
fetch('/data')
.then((response) => {
response.json().then((data) => {
callback(data, 'value', 'label');
});
})
.catch((error) => {
console.error(error);
});
}).setChoices(async () => {
const data = await fetch('/data');
return data.json();
});
new Choices('#choices-scrolling-dropdown', {
@ -331,10 +450,12 @@
new Choices('#choices-within-form');
new Choices('#choices-set-choice-by-value').setChoiceByValue('Choice 2');
new Choices('#choices-set-choice-by-value').setChoiceByValue(
'Choice 2',
);
new Choices('#choices-search-by-label', { searchFields: ['label'] });
});
</script>
</body>
</html>
</html>

View File

@ -1,28 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,user-scalable=no"
/>
<title>Choices</title>
<meta
name="description"
itemprop="description"
content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency."
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="../assets/images/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-32x32.png"
sizes="32x32"
/>
<link
rel="icon"
type="image/png"
href="../assets/images/favicon-16x16.png"
sizes="16x16"
/>
<link rel="manifest" href="../assets/images/manifest.json" />
<link
rel="mask-icon"
href="../assets/images/safari-pinned-tab.svg"
color="#00bcd4"
/>
<link rel="shortcut icon" href="../assets/images/favicon.ico" />
<meta
name="msapplication-config"
content="../assets/images/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Choices</title>
<meta name=description itemprop=description content="A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.">
<link rel="apple-touch-icon" sizes="180x180" href="../assets/images/apple-touch-icon.png">
<link rel="icon" type="image/png" href="../assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="../assets/images/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="../assets/images/manifest.json">
<link rel="mask-icon" href="../assets/images/safari-pinned-tab.svg" color="#00bcd4">
<link rel="shortcut icon" href="../assets/images/favicon.ico">
<meta name="msapplication-config" content="../assets/images/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3" />
<!-- End ignore these -->
<!-- Ignore these -->
<link rel="stylesheet" href="../assets/styles/base.min.css?version=6.0.3">
<!-- End ignore these -->
<!-- Choices includes -->
<link rel="stylesheet" href="../assets/styles/choices.min.css?version=6.0.3">
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
<!-- Choices includes -->
<link
rel="stylesheet"
href="../assets/styles/choices.min.css?version=6.0.3"
/>
<script src="../assets/scripts/choices.min.js?version=6.0.3"></script>
<!-- End Choices includes -->
</head>
<body>
@ -43,7 +73,11 @@
<div data-test-hook="remove-button">
<label for="choices-remove-button">Remove button</label>
<select class="form-control" name="choices-remove-button" id="choices-remove-button">
<select
class="form-control"
name="choices-remove-button"
id="choices-remove-button"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -53,7 +87,11 @@
<div data-test-hook="disabled-choice">
<label for="choices-disabled-choice">Disabled choice</label>
<select class="form-control" name="choices-disabled-choice" id="choices-disabled-choice">
<select
class="form-control"
name="choices-disabled-choice"
id="choices-disabled-choice"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -63,7 +101,11 @@
<div data-test-hook="add-items-disabled">
<label for="choices-add-items-disabled">Add items disabled</label>
<select class="form-control" name="choices-add-items-disabled" id="choices-add-items-disabled">
<select
class="form-control"
name="choices-add-items-disabled"
id="choices-add-items-disabled"
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -72,7 +114,12 @@
<div data-test-hook="disabled-via-attr">
<label for="choices-disabled-via-attr">Disabled via attribute</label>
<select class="form-control" name="choices-disabled-via-attr" id="choices-disabled-via-attr" disabled>
<select
class="form-control"
name="choices-disabled-via-attr"
id="choices-disabled-via-attr"
disabled
>
<option value="Choice 1" selected>Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -81,7 +128,11 @@
<div data-test-hook="prepend-append">
<label for="choices-prepend-append">Prepend/append</label>
<select class="form-control" name="choices-prepend-append" id="choices-prepend-append">
<select
class="form-control"
name="choices-prepend-append"
id="choices-prepend-append"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -90,7 +141,11 @@
<div data-test-hook="render-choice-limit">
<label for="choices-render-choice-limit">Render choice limit</label>
<select class="form-control" name="choices-render-choice-limit" id="choices-render-choice-limit">
<select
class="form-control"
name="choices-render-choice-limit"
id="choices-render-choice-limit"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -99,7 +154,11 @@
<div data-test-hook="search-disabled">
<label for="choices-search-disabled">Search disabled</label>
<select class="form-control" name="choices-search-disabled" id="choices-search-disabled">
<select
class="form-control"
name="choices-search-disabled"
id="choices-search-disabled"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -108,7 +167,11 @@
<div data-test-hook="search-floor">
<label for="choices-search-floor">Search floor</label>
<select class="form-control" name="choices-search-floor" id="choices-search-floor">
<select
class="form-control"
name="choices-search-floor"
id="choices-search-floor"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -117,12 +180,20 @@
<div data-test-hook="remote-data">
<label for="choices-remote-data">Remote data</label>
<select class="form-control" name="choices-remote-data" id="choices-remote-data"></select>
<select
class="form-control"
name="choices-remote-data"
id="choices-remote-data"
></select>
</div>
<div data-test-hook="scrolling-dropdown">
<label for="choices-scrolling-dropdown">Scrolling dropdown</label>
<select class="form-control" name="choices-scrolling-dropdown" id="choices-scrolling-dropdown">
<select
class="form-control"
name="choices-scrolling-dropdown"
id="choices-scrolling-dropdown"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -143,7 +214,12 @@
<div data-test-hook="groups">
<label for="choices-groups">Choice groups</label>
<select class="form-control" name="choices-groups" id="choices-groups" multiple>
<select
class="form-control"
name="choices-groups"
id="choices-groups"
multiple
>
<optgroup label="UK">
<option value="London">London</option>
<option value="Manchester">Manchester</option>
@ -159,7 +235,11 @@
<div data-test-hook="parent-child">
<label for="choices-parent">Parent</label>
<select class="form-control" name="choices-parent" id="choices-parent">
<select
class="form-control"
name="choices-parent"
id="choices-parent"
>
<option value="Parent choice 1">Parent choice 1</option>
<option value="Parent choice 2">Parent choice 2</option>
<option value="Parent choice 3">Parent choice 3</option>
@ -175,26 +255,44 @@
<div data-test-hook="custom-properties">
<label for="choices-custom-properties">Custom properties</label>
<select class="form-control" name="choices-custom-properties" id="choices-custom-properties"></select>
<select
class="form-control"
name="choices-custom-properties"
id="choices-custom-properties"
></select>
</div>
<div data-test-hook="non-string-values">
<label for="choices-non-string-values">Non-string values</label>
<select class="form-control" name="choices-non-string-values" id="choices-non-string-values"></select>
<select
class="form-control"
name="choices-non-string-values"
id="choices-non-string-values"
></select>
</div>
<div data-test-hook="within-form">
<form>
<label for="choices-within-form">Within form</label>
<select class="form-control" name="choices-within-form" id="choices-within-form">
<select
class="form-control"
name="choices-within-form"
id="choices-within-form"
>
<option value="Choice 1">Choice 1</option>
</select>
</form>
</div>
<div data-test-hook="set-choice-by-value">
<label for="choices-set-choice-by-value">Dynamically set choice by value</label>
<select class="form-control" name="choices-set-choice-by-value" id="choices-set-choice-by-value">
<label for="choices-set-choice-by-value"
>Dynamically set choice by value</label
>
<select
class="form-control"
name="choices-set-choice-by-value"
id="choices-set-choice-by-value"
>
<option value="Choice 1">Choice 1</option>
<option value="Choice 2">Choice 2</option>
<option value="Choice 3">Choice 3</option>
@ -203,7 +301,11 @@
<div data-test-hook="search-by-label">
<label for="choices-search-by-label">Search by label</label>
<select class="form-control" name="choices-search-by-label" id="choices-search-by-label">
<select
class="form-control"
name="choices-search-by-label"
id="choices-search-by-label"
>
<option value="value1">label1</option>
<option value="value2">label2</option>
</select>
@ -214,13 +316,17 @@
document.addEventListener('DOMContentLoaded', function() {
const choicesBasic = new Choices('#choices-basic');
document.querySelector('button.disable').addEventListener('click', () => {
choicesBasic.disable();
});
document
.querySelector('button.disable')
.addEventListener('click', () => {
choicesBasic.disable();
});
document.querySelector('button.enable').addEventListener('click', () => {
choicesBasic.enable();
});
document
.querySelector('button.enable')
.addEventListener('click', () => {
choicesBasic.enable();
});
new Choices('#choices-remove-button', {
removeItemButton: true,
@ -242,12 +348,12 @@
});
new Choices('#choices-render-choice-limit', {
renderChoiceLimit: 1
renderChoiceLimit: 1,
});
new Choices('#choices-search-disabled', {
searchEnabled: false
})
searchEnabled: false,
});
new Choices('#choices-search-floor', {
searchFloor: 5,
@ -255,16 +361,9 @@
new Choices('#choices-remote-data', {
shouldSort: false,
}).ajax((callback) => {
fetch('/data')
.then((response) => {
response.json().then((data) => {
callback(data, 'value', 'label');
});
})
.catch((error) => {
console.error(error);
});
}).setChoices(async () => {
const res = await fetch('/data');
return res.json();
});
new Choices('#choices-scrolling-dropdown', {
@ -276,7 +375,7 @@
const parent = new Choices('#choices-parent');
const child = new Choices('#choices-child').disable();
parent.passedElement.element.addEventListener('change', (event) => {
parent.passedElement.element.addEventListener('change', event => {
if (event.detail.value === 'Parent choice 2') {
child.enable();
} else {
@ -310,8 +409,8 @@
customProperties: {
country: 'Portugal',
},
}
]
},
],
});
new Choices('#choices-non-string-values', {
@ -343,10 +442,12 @@
new Choices('#choices-within-form');
new Choices('#choices-set-choice-by-value').setChoiceByValue('Choice 2');
new Choices('#choices-set-choice-by-value').setChoiceByValue(
'Choice 2',
);
new Choices('#choices-search-by-label', { searchFields: ['label'] });
});
</script>
</body>
</html>
</html>

View File

@ -31,7 +31,6 @@ import {
sortByScore,
generateId,
findAncestorByAttrName,
fetchFromObject,
isIE11,
existsInArray,
cloneObject,
@ -44,6 +43,10 @@ const USER_DEFAULTS = /** @type {Partial<import('../../types/index').Choices.Opt
* Choices
* @author Josh Johnson<josh@joshuajohnson.co.uk>
*/
/**
* @typedef {import('../../types/index').Choices.Choice} Choice
*/
class Choices {
/* ========================================
= Static properties =
@ -222,7 +225,7 @@ class Choices {
}
/* ========================================
= Public functions =
= Public methods =
======================================== */
init() {
@ -460,9 +463,93 @@ class Choices {
return this;
}
setChoices(choices = [], value = '', label = '', replaceChoices = false) {
if (!this._isSelectElement || !value) {
return this;
/**
* Set choices of select input via an array of objects (or function that returns array of object or promise of it),
* a value field name and a label field name.
* This behaves the same as passing items via the choices option but can be called after initialising Choices.
* This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices.
* Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc).
*
* **Input types affected:** select-one, select-multiple
*
* @template {object[] | ((instance: Choices) => object[] | Promise<object[]>)} T
* @param {T} [choicesArrayOrFetcher]
* @param {string} [value = 'value'] - name of `value` field
* @param {string} [label = 'label'] - name of 'label' field
* @param {boolean} [replaceChoices = false] - whether to replace of add choices
* @returns {this | Promise<this>}
*
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices([
* {value: 'One', label: 'Label One', disabled: true},
* {value: 'Two', label: 'Label Two', selected: true},
* {value: 'Three', label: 'Label Three'},
* ], 'value', 'label', false);
* ```
*
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices(async () => {
* try {
* const items = await fetch('/items');
* return items.json()
* } catch(err) {
* console.error(err)
* }
* });
* ```
*
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices([{
* label: 'Group one',
* id: 1,
* disabled: false,
* choices: [
* {value: 'Child One', label: 'Child One', selected: true},
* {value: 'Child Two', label: 'Child Two', disabled: true},
* {value: 'Child Three', label: 'Child Three'},
* ]
* },
* {
* label: 'Group two',
* id: 2,
* disabled: false,
* choices: [
* {value: 'Child Four', label: 'Child Four', disabled: true},
* {value: 'Child Five', label: 'Child Five'},
* {value: 'Child Six', label: 'Child Six', customProperties: {
* description: 'Custom description about child six',
* random: 'Another random custom property'
* }},
* ]
* }], 'value', 'label', false);
* ```
*/
setChoices(
choicesArrayOrFetcher = [],
value = 'value',
label = 'label',
replaceChoices = false,
) {
if (!this.initialised)
throw new ReferenceError(
`setChoices was called on a non-initialized instance of Choices`,
);
if (!this._isSelectElement)
throw new TypeError(`setChoices can't be used with INPUT based Choices`);
if (typeof value !== 'string' || !value) {
throw new TypeError(
`value parameter must be a name of 'value' field in passed objects`,
);
}
// Clear choices if needed
@ -470,6 +557,34 @@ class Choices {
this.clearChoices();
}
if (!Array.isArray(choicesArrayOrFetcher)) {
if (typeof choicesArrayOrFetcher !== 'function')
throw new TypeError(
`.setChoices must be called either with array of choices with a function resulting into Promise of array of choices`,
);
// it's a choices fetcher
requestAnimationFrame(() => this._handleLoadingState(true));
const fetcher = choicesArrayOrFetcher(this);
if (typeof fetcher === 'object' && typeof fetcher.then === 'function') {
// that's a promise
return fetcher
.then(data => this.setChoices(data, value, label, replaceChoices))
.catch(err => {
if (!this.config.silent) console.error(err);
})
.then(() => this._handleLoadingState(false))
.then(() => this);
}
// function returned something else than promise, let's check if it's an array of choices
if (!Array.isArray(fetcher))
throw new TypeError(
`.setChoices first argument function must return either array of choices or Promise, got: ${typeof fetcher}`,
);
// recursion with results, it's sync and choices were cleared already
return this.setChoices(fetcher, value, label, false);
}
this.containerOuter.removeLoadingState();
const addGroupsAndChoices = groupOrChoice => {
if (groupOrChoice.choices) {
@ -492,7 +607,7 @@ class Choices {
};
this._setLoading(true);
choices.forEach(addGroupsAndChoices);
choicesArrayOrFetcher.forEach(addGroupsAndChoices);
this._setLoading(false);
return this;
@ -519,18 +634,7 @@ class Choices {
return this;
}
ajax(fn) {
if (!this.initialised || !this._isSelectElement || !fn) {
return this;
}
requestAnimationFrame(() => this._handleLoadingState(true));
fn(this._ajaxCallback());
return this;
}
/* ===== End of Public functions ====== */
/* ===== End of Public methods ====== */
/* =============================================
= Private functions =
@ -1054,55 +1158,6 @@ class Choices {
};
}
_ajaxCallback() {
return (results, value, label) => {
if (!results || !value) {
return;
}
const parsedResults = isType('Object', results) ? [results] : results;
if (
parsedResults &&
isType('Array', parsedResults) &&
parsedResults.length
) {
// Remove loading states/text
this._handleLoadingState(false);
this._setLoading(true);
// Add each result as a choice
parsedResults.forEach(result => {
if (result.choices) {
this._addGroup({
group: result,
id: result.id || null,
valueKey: value,
labelKey: label,
});
} else {
this._addChoice({
value: fetchFromObject(result, value),
label: fetchFromObject(result, label),
isSelected: result.selected,
isDisabled: result.disabled,
customProperties: result.customProperties,
placeholder: result.placeholder,
});
}
});
this._setLoading(false);
if (this._isSelectOneElement) {
this._selectPlaceholderChoice();
}
} else {
// No results, remove loading state
this._handleLoadingState(false);
}
};
}
_searchChoices(value) {
const newValue = isType('String', value) ? value.trim() : value;
const currentValue = isType('String', this._currentValue)

View File

@ -881,84 +881,74 @@ describe('choices', () => {
});
});
describe('ajax', () => {
const callbackoutput = 'worked';
let handleLoadingStateStub;
let ajaxCallbackStub;
const returnsEarly = () => {
it('returns early', () => {
expect(handleLoadingStateStub.called).to.equal(false);
expect(ajaxCallbackStub.called).to.equal(false);
});
};
beforeEach(() => {
handleLoadingStateStub = stub();
ajaxCallbackStub = stub().returns(callbackoutput);
instance._ajaxCallback = ajaxCallbackStub;
instance._handleLoadingState = handleLoadingStateStub;
});
afterEach(() => {
instance._ajaxCallback.reset();
instance._handleLoadingState.reset();
});
describe('setChoices with callback/Promise', () => {
describe('not initialised', () => {
beforeEach(() => {
instance.initialised = false;
output = instance.ajax(() => {});
});
returnsInstance(output);
returnsEarly();
it('should throw', () => {
expect(() => instance.setChoices(null)).Throw(ReferenceError);
});
});
describe('text element', () => {
beforeEach(() => {
instance._isSelectElement = false;
output = instance.ajax(() => {});
});
returnsInstance(output);
returnsEarly();
it('should throw', () => {
expect(() => instance.setChoices(null)).Throw(TypeError);
});
});
describe('passing invalid function', () => {
beforeEach(() => {
output = instance.ajax(null);
instance._isSelectElement = true;
});
returnsInstance(output);
returnsEarly();
it('should throw on non function', () => {
expect(() => instance.setChoices(null)).Throw(TypeError, /Promise/i);
});
it(`should throw on function that doesn't return promise`, () => {
expect(() => instance.setChoices(() => 'boo')).to.throw(
TypeError,
/promise/i,
);
});
});
describe('select element', () => {
let callback;
it('fetches and sets choices', async () => {
document.body.innerHTML = '<select id="test" />';
const choice = new Choices('#test');
const handleLoadingStateSpy = spy(choice, '_handleLoadingState');
beforeEach(() => {
instance.initialised = true;
instance._isSelectElement = true;
ajaxCallbackStub = stub();
callback = stub();
output = instance.ajax(callback);
});
returnsInstance(output);
it('sets loading state', done => {
requestAnimationFrame(() => {
expect(handleLoadingStateStub.called).to.equal(true);
done();
});
});
it('calls passed function with ajax callback', () => {
expect(callback.called).to.equal(true);
expect(callback.lastCall.args[0]).to.eql(callbackoutput);
let fetcherCalled = false;
const fetcher = async inst => {
expect(inst).to.eq(choice);
fetcherCalled = true;
await new Promise(resolve => setTimeout(resolve, 1000));
return [
{ label: 'l1', value: 'v1', customProperties: 'prop1' },
{ label: 'l2', value: 'v2', customProperties: 'prop2' },
];
};
expect(choice._store.choices.length).to.equal(0);
const promise = choice.setChoices(fetcher);
await new Promise(resolve =>
requestAnimationFrame(() => {
expect(handleLoadingStateSpy.callCount).to.equal(1);
resolve();
}),
);
expect(fetcherCalled).to.be.true;
const res = await promise;
expect(res).to.equal(choice);
expect(choice._store.choices[1].value).to.equal('v2');
expect(choice._store.choices[1].label).to.equal('l2');
expect(choice._store.choices[1].customProperties).to.equal('prop2');
});
});
});
@ -1353,31 +1343,29 @@ describe('choices', () => {
instance.containerOuter.removeLoadingState.reset();
});
const returnsEarly = () => {
it('returns early', () => {
expect(addGroupStub.called).to.equal(false);
expect(addChoiceStub.called).to.equal(false);
expect(clearChoicesStub.called).to.equal(false);
});
};
describe('when element is not select element', () => {
beforeEach(() => {
instance._isSelectElement = false;
instance.setChoices(choices, value, label, false);
});
returnsEarly();
it('throws', () => {
expect(() =>
instance.setChoices(choices, value, label, false),
).to.throw(TypeError, /input/i);
});
});
describe('passing invalid arguments', () => {
describe('passing no value', () => {
beforeEach(() => {
instance._isSelectElement = true;
instance.setChoices(choices, undefined, 'label', false);
});
returnsEarly();
it('throws', () => {
expect(() =>
instance.setChoices(choices, null, 'label', false),
).to.throw(TypeError, /value/i);
});
});
});

View File

@ -142,19 +142,6 @@ export const getWindowHeight = () => {
);
};
export const fetchFromObject = (object, path) => {
const index = path.indexOf('.');
if (index > -1) {
return fetchFromObject(
object[path.substring(0, index)],
path.substr(index + 1),
);
}
return object[path];
};
export const isIE11 = () =>
!!(
navigator.userAgent.match(/Trident/) &&

View File

@ -10,7 +10,6 @@ import {
sanitise,
sortByAlpha,
sortByScore,
fetchFromObject,
existsInArray,
cloneObject,
dispatchEvent,
@ -198,19 +197,6 @@ describe('utils', () => {
});
});
describe('fetchFromObject', () => {
it('fetches value from object using given path', () => {
const object = {
band: {
name: 'The Strokes',
},
};
const output = fetchFromObject(object, 'band.name');
expect(output).to.equal(object.band.name);
});
});
describe('existsInArray', () => {
it('determines whether a value exists within given array', () => {
const values = [

112
types/index.d.ts vendored
View File

@ -872,16 +872,47 @@ export default class Choices {
*/
getValue(valueOnly?: boolean): string | string[];
/** Direct populate choices
*
* @param {string[] | Choices.Item[]} items
*/
setValue(items: string[] | Choices.Item[]): this;
/**
* Set choices of select input via an array of objects, a value name and a label name.
* Set value of input based on existing Choice. `value` can be either a single string or an array of strings
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* const example = new Choices(element, {
* choices: [
* {value: 'One', label: 'Label One'},
* {value: 'Two', label: 'Label Two', disabled: true},
* {value: 'Three', label: 'Label Three'},
* ],
* });
*
* example.setChoiceByValue('Two'); // Choice with value of 'Two' has now been selected.
* ```
*/
setChoiceByValue(value: string | string[]): this;
/**
* Set choices of select input via an array of objects (or function that returns array of object or promise of it),
* a value field name and a label field name.
* This behaves the same as passing items via the choices option but can be called after initialising Choices.
* This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices.
* Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc).
*
* **Input types affected:** select-one, select-multiple
*
* @example Example 1:
* ```
* @param {string} [value = 'value'] - name of `value` field
* @param {string} [label = 'label'] - name of 'label' field
* @param {boolean} [replaceChoices = false] - whether to replace of add choices
*
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices([
@ -891,8 +922,22 @@ export default class Choices {
* ], 'value', 'label', false);
* ```
*
* @example Example 2:
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices(async () => {
* try {
* const items = await fetch('/items');
* return items.json()
* } catch(err) {
* console.error(err)
* }
* });
* ```
*
* @example
* ```js
* const example = new Choices(element);
*
* example.setChoices([{
@ -920,35 +965,14 @@ export default class Choices {
* }], 'value', 'label', false);
* ```
*/
setValue(args: string[]): this;
/**
* Set value of input based on existing Choice. `value` can be either a single string or an array of strings
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* const example = new Choices(element, {
* choices: [
* {value: 'One', label: 'Label One'},
* {value: 'Two', label: 'Label Two', disabled: true},
* {value: 'Three', label: 'Label Three'},
* ],
* });
*
* example.setChoiceByValue('Two'); // Choice with value of 'Two' has now been selected.
* ```
*/
setChoiceByValue(value: string | string[]): this;
/** Direct populate choices */
setChoices(
choices: Choices.Choice[],
value: string,
label: string,
setChoices<
T extends object[] | ((instance: Choices) => object[] | Promise<object[]>)
>(
choices: T,
value?: string,
label?: string,
replaceChoices?: boolean,
): this;
): T extends object[] ? this : Promise<this>;
/**
* Clear all choices from select.
@ -984,28 +1008,4 @@ export default class Choices {
* **Input types affected:** text, select-one, select-multiple
*/
disable(): this;
/**
* Populate choices/groups via a callback.
*
* **Input types affected:** select-one, select-multiple
*
* @example
* ```
* var example = new Choices(element);
*
* example.ajax(function(callback) {
* fetch(url)
* .then(function(response) {
* response.json().then(function(data) {
* callback(data, 'value', 'label');
* });
* })
* .catch(function(error) {
* console.log(error);
* });
* });
* ```
*/
ajax(fn: (values: any) => any): this;
}