Skip to content

Commit

Permalink
server side support
Browse files Browse the repository at this point in the history
  • Loading branch information
lekoala committed Dec 13, 2021
1 parent 00bb427 commit acbf2e3
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 39 deletions.
8 changes: 8 additions & 0 deletions demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ <h1>Demo</h1>
<div class="invalid-feedback">Please select a valid tag.</div>
</div>
</div>
<div class="row mb-3 g-3">
<div class="col-md-4">
<label for="validationTagsJson" class="form-label">Tags (server side)</label>
<select class="form-select" id="validationTagsJson" name="tags_json[]" multiple data-allow-new="true" data-server="demo.json" data-live-server="1">
<option disabled hidden value="">Choose a tag...</option>
</select>
</div>
</div>
<button class="btn btn-primary" type="submit">Submit form</button>
</form>
</div>
Expand Down
18 changes: 18 additions & 0 deletions demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"value": "server1",
"label": "Server 1"
},
{
"value": "server2",
"label": "Server Orange",
"data": {
"badgeStyle": "warning",
"badgeClass": "text-dark"
}
},
{
"value": "server3",
"label": "Server 3"
}
]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bootstrap5-tags",
"version": "1.2.4",
"version": "1.3.0",
"description": "Replace select[multiple] with nices badges",
"main": "tags",
"scripts": {
Expand Down
14 changes: 14 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ Use attribute `data-suggestions-threshold` to determine how many characters need

*NOTE: don't forget the [] if you need multiple values!*

## Server side support

You can also use options provided by the server. This script expects a json response that is an array or an object with the data key containing an array.

Simply set `data-server` where your endpoint is located. It should provide an array of value/label objects. The suggestions will be populated upon init
except if `data-live-server` is set, in which case, it will be populated on type. A ?query= parameter is passed along with the current value of the searchInput.

```html
<label for="validationTagsJson" class="form-label">Tags (server side)</label>
<select class="form-select" id="validationTagsJson" name="tags_json[]" multiple data-allow-new="true" data-server="demo.json" data-live-server="1">
<option disabled hidden value="">Choose a tag...</option>
</select>
```

## Accessibility

You can set accessibility labels when passing options:
Expand Down
128 changes: 93 additions & 35 deletions tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ class Tags {
this.placeholder = this.getPlaceholder();
this.allowNew = selectElement.dataset.allowNew ? true : false;
this.showAllSuggestions = selectElement.dataset.showAllSuggestions ? true : false;
this.badgeStyle = selectElement.dataset.badgeStyle ?? "primary";
this.badgeStyle = selectElement.dataset.badgeStyle || "primary";
this.allowClear = selectElement.dataset.allowClear ? true : false;
this.server = selectElement.dataset.server || false;
this.liveServer = selectElement.dataset.liveServer ? true : false;
this.suggestionsThreshold = selectElement.dataset.suggestionsThreshold ? parseInt(selectElement.dataset.suggestionsThreshold) : 1;
this.keyboardNavigation = false;
this.clearLabel = opts.clearLabel ?? "Clear";
this.searchLabel = opts.searchLabel ?? "Type a value";
this.clearLabel = opts.clearLabel || "Clear";
this.searchLabel = opts.searchLabel || "Type a value";

// Create elements
this.holderElement = document.createElement("div"); // this is the one holding the fake input and the dropmenu
Expand All @@ -50,7 +52,18 @@ class Tags {
this.configureHolderElement();
this.configureDropElement();
this.configureContainerElement();
this.buildSuggestions();

if (this.server && !this.liveServer) {
this.loadFromServer();
} else {
let suggestions = Array.from(this.selectElement.querySelectorAll("option")).map((option) => {
return {
value: option.getAttribute("value"),
label: option.innerText,
};
});
this.buildSuggestions(suggestions);
}
}

/**
Expand All @@ -66,6 +79,32 @@ class Tags {
}
}

/**
* @param {boolean} show
*/
loadFromServer(show = false) {
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
fetch(this.server + "?query=" + encodeURIComponent(this.searchInput.value), { signal: this.abortController.signal })
.then((r) => r.json())
.then((suggestions) => {
let data = suggestions.data || suggestions;
this.buildSuggestions(data);
this.abortController = null;
if (show) {
this.showSuggestions();
}
})
.catch((e) => {
if (e.name === "AbortError") {
return;
}
console.error(e);
});
}

/**
* @returns {string}
*/
Expand Down Expand Up @@ -139,7 +178,11 @@ class Tags {
this.searchInput.addEventListener("input", (event) => {
this.adjustWidth();
if (this.searchInput.value.length >= this.suggestionsThreshold) {
this.showSuggestions();
if (this.liveServer) {
this.loadFromServer(true);
} else {
this.showSuggestions();
}
} else {
this.hideSuggestions();
}
Expand All @@ -161,14 +204,11 @@ class Tags {
case "Enter":
let selection = this.getActiveSelection();
if (selection) {
this.addItem(selection.innerText, selection.getAttribute(VALUE_ATTRIBUTE));
this.resetSearchInput();
this.hideSuggestions();
this.removeActiveSelection();
selection.click();
} else {
// We use what is typed
if (this.allowNew) {
let res = this.addItem(this.searchInput.value, null, true);
if (this.allowNew && !this.isSelected(this.searchInput.value)) {
let res = this.addItem(this.searchInput.value, null);
if (res) {
this.resetSearchInput();
this.hideSuggestions();
Expand Down Expand Up @@ -274,22 +314,30 @@ class Tags {
}

/**
* Add suggestions from element
* Add suggestions to the drop element
* @param {array}
*/
buildSuggestions() {
let options = this.selectElement.querySelectorAll("option");
for (let i = 0; i < options.length; i++) {
let opt = options[i];
if (!opt.getAttribute("value")) {
buildSuggestions(suggestions = null) {
while (this.dropElement.lastChild) {
this.dropElement.removeChild(this.dropElement.lastChild);
}
for (let i = 0; i < suggestions.length; i++) {
let suggestion = suggestions[i];
if (!suggestion.value) {
continue;
}
let newChild = document.createElement("li");
let newChildLink = document.createElement("a");
newChild.append(newChildLink);
newChildLink.classList.add("dropdown-item");
newChildLink.setAttribute(VALUE_ATTRIBUTE, opt.getAttribute("value"));
newChildLink.setAttribute(VALUE_ATTRIBUTE, suggestion.value);
newChildLink.setAttribute("href", "#");
newChildLink.innerText = opt.innerText;
newChildLink.innerText = suggestion.label;
if (suggestion.data) {
for (const [key, value] of Object.entries(suggestion.data)) {
newChildLink.dataset[key] = value;
}
}
this.dropElement.appendChild(newChild);

// Hover sets active item
Expand All @@ -312,7 +360,7 @@ class Tags {
});
newChildLink.addEventListener("click", (event) => {
event.preventDefault();
this.addItem(newChildLink.innerText, newChildLink.getAttribute(VALUE_ATTRIBUTE));
this.addItem(newChildLink.innerText, newChildLink.getAttribute(VALUE_ATTRIBUTE), newChildLink.dataset);
this.resetSearchInput();
this.hideSuggestions();
});
Expand Down Expand Up @@ -450,40 +498,46 @@ class Tags {
return ver;
}

/**
* Find if label is already selected
* @param {string} text
* @returns {boolean}
*/
isSelected(text) {
const opt = Array.from(this.selectElement.querySelectorAll("option")).find((el) => el.textContent == text);
if (opt && opt.getAttribute("selected")) {
return true;
}
return false;
}

/**
* @param {string} text
* @param {string} value
* @param {boolean} checkSelected
* @param {object} data
* @return {boolean}
*/
addItem(text, value, checkSelected = false) {
addItem(text, value = null, data = {}) {
if (!value) {
value = text;
}

const bver = this.getBootstrapVersion();

// Find by label and value
let opt = this.selectElement.querySelector('option[value="' + value + '"]');
if (!opt) {
opt = Array.from(this.selectElement.querySelectorAll("option")).find((el) => el.textContent == text);
}
if (checkSelected) {
if (opt && opt.getAttribute("selected")) {
return false;
}
if (opt) {
data = opt.dataset;
}

// create span
let html = text;
let span = document.createElement("span");
let badgeStyle = this.badgeStyle;
span.classList.add("badge");
if (opt && opt.dataset.badgeStyle) {
badgeStyle = opt.dataset.badgeStyle;
if (data.badgeStyle) {
badgeStyle = data.badgeStyle;
}
if (opt && opt.dataset.badgeClass) {
span.classList.add(opt.dataset.badgeClass);
if (data.badgeClass) {
span.classList.add(data.badgeClass);
}
if (bver === 5) {
//https://getbootstrap.com/docs/5.1/components/badge/
Expand Down Expand Up @@ -524,6 +578,10 @@ class Tags {
opt = document.createElement("option");
opt.value = value;
opt.innerText = text;
// Pass along data provided
for (const [key, value] of Object.entries(data)) {
opt.dataset[key] = value;
}
opt.setAttribute("selected", "selected");
this.selectElement.appendChild(opt);
}
Expand Down
2 changes: 1 addition & 1 deletion tags.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions tags.min.js.map

Large diffs are not rendered by default.

0 comments on commit acbf2e3

Please sign in to comment.