Skip to content
This repository has been archived by the owner on Nov 25, 2020. It is now read-only.

feat(project): add items bindable #2

Merged
merged 10 commits into from Jan 11, 2017
27 changes: 24 additions & 3 deletions README.md
Expand Up @@ -78,12 +78,16 @@ export class MyViewModel {

### Bindables

#### limit;
#### limit = 10;
the max amount of results to return. (optional)

#### items;
used when one already has a list of items to filter on. Requests is not
necessary.

#### resource;
the string that is appended to the api endpoint. e.g. api.com/language.
language is the resource.
the string that is appended to the api endpoint. e.g. `language` is the
resource in following url `api.com/language`

#### search = '';
the string to be used to do a contains search with. By default it will look
Expand Down Expand Up @@ -130,6 +134,23 @@ Overwrite the default fetch. Expects to return a Promise which resolves to
a list of results

#### searchQuery(string)

By default aurelia autocomplete works with waterline queries. If your query
language differs, you can overwrite this method to return a query your endpoint
accepts.

#### filter(items)

Takes items and returns only the array defined in the items bindable and should
returns a new array containing the desired array with results.

By default it makes sure to not return more items than the limit specifies.

#### itemMatches(item)

Used by the `this.filter` to determine if an item should be added or not.

#### get regex()

Returns a regex which is used for highlighting the items in the html and for
determining if an item matches in the itemMatches method
135 changes: 85 additions & 50 deletions src/component/autocomplete.js
@@ -1,14 +1,14 @@
import {inject, bindable, TaskQueue, bindingMode} from 'aurelia-framework';
import {Config} from 'aurelia-api';
import {logger} from '../aurelia-autocomplete';
import {DOM} from 'aurelia-pal';
import {resolvedView} from 'aurelia-view-manager';
import {computedFrom, inject, bindable, TaskQueue, bindingMode} from 'aurelia-framework';
import {Config} from 'aurelia-api';
import {logger} from '../aurelia-autocomplete';
import {DOM} from 'aurelia-pal';
import {resolvedView} from 'aurelia-view-manager';

@resolvedView('spoonx/auto-complete', 'autocomplete')
@inject(DOM, Config, DOM.Element, TaskQueue)
export class AutoCompleteCustomElement {

showResults = false;
lastFindPromise;

// the query string is set after selecting an option. To avoid this
// triggering a new query we set the justSelected to true. When true it will
Expand All @@ -20,12 +20,16 @@ export class AutoCompleteCustomElement {
liEventListeners = [];

//the max amount of results to return. (optional)
@bindable limit;
@bindable limit = 10;

//the string that is appended to the api endpoint. e.g. api.com/language.
//language is the resource.
@bindable resource;

// used when one already has a list of items to filter on. Requests is not
// necessary
@bindable items;

//the string to be used to do a contains search with. By default it will look
//if the name contains this value
@bindable search = '';
Expand Down Expand Up @@ -64,10 +68,13 @@ export class AutoCompleteCustomElement {
}

bind() {
if (!this.resource) {
return logger.error('auto complete requires resource to be defined');
if (!this.resource && !this.items) {
return logger.error('auto complete requires resource or items bindable to be defined');
}

this.search = this.label(this.value);
this.justSelected = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sloppy code style


this.apiEndpoint = this.apiEndpoint.getEndpoint(this.endpoint);
}

Expand Down Expand Up @@ -125,35 +132,35 @@ export class AutoCompleteCustomElement {
}

/**
* returns HTML that wraps matching substrings with strong tags
* returns HTML that wraps matching substrings with strong tags.
* If not a "stringable" it returns an empty string.
*
* @param {Object} result
*
* @returns {String}
*/
labelWithMatches(result) {
return this.label(result).replace(
new RegExp(this.search, 'gi'),
match => {
return `<strong>${match}</strong>`;
});
let label = this.label(result);

if (!label.replace) {
return '';
}

return label.replace(this.regex, match => {
return `<strong>${match}</strong>`;
});
}

/**
* Prepares the DOM by adding event listeners
*/
attached() {
this.inputElement = this.element.querySelectorAll('input')[0];
this.dropdownElement = this.element.querySelectorAll('#dropdown')[0];
this.dropdownElement = this.element.querySelectorAll('.dropdown.open')[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because bootstrap.js will add and remove the open class for you


let openDropdown = (show = true) => {
this.showResults = show;
};

this.inputElement.addEventListener('focus', openDropdown);
this.inputElement.addEventListener('click', openDropdown);

this.inputElement.addEventListener('blur', () => openDropdown(false));
this.registerKeyDown(this.inputElement, '*', event => {
this.dropdownElement.className = 'dropdown open';
});

this.registerKeyDown(this.inputElement, 'down', event => {
this.selected = this.nextFoundResult(this.selected);
Expand Down Expand Up @@ -201,7 +208,6 @@ export class AutoCompleteCustomElement {
this.results = [];
this.justSelected = true;
this.search = this.label(this.value);
this.showResults = false;
}

/**
Expand All @@ -211,49 +217,76 @@ export class AutoCompleteCustomElement {
* @param {string} newValue
* @param {string} oldValue
*
* @returns {void}
* @returns {Promise}
*/
searchChanged(newValue, oldValue) {
if (!this.shouldPerformRequest()) {
this.results = [];

return;
return Promise.resolve();
}

return this.findResults(this.searchQuery(this.search)).then(results => {
// when resource is not defined it will not perform a request. Instead it
// will search for the first items that pass the predicate
if (this.items) {
this.results = this.sort(this.filter(this.items));

return Promise.resolve();
}

this.lastFindPromise = this.findResults(this.searchQuery(this.search)).then(results => {
if (this.lastFindPromise !== promise) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed I guess

return;
}

this.lastFindPromise = false;

this.results = this.sort(results);

if (this.results.length !== 0) {
this.selected = this.results[0];
this.value = this.selected;
this.showResults = true;
}
});
}

/**
* when the results change, add the event listeners to the li items.
* This has to be done because bootstrap will otherwise overrule the events
* returns a list of length that is smaller or equal to the limit. The
* default predicate is based on the regex
*
* @param {Object[]} items
*
* @returns {Object[]}
*/
resultsChanged() {
this.removeEventListeners(this.liEventListeners);
this.liEventListeners = [];

this.queue.queueTask(() => {
this.element.querySelectorAll('li').forEach((li, index) => {
let clickEventFunction = () => {
this.onSelect(this.results[index])
};

li.addEventListener('click', clickEventFunction);

this.liEventListeners.push({
callback : clickEventFunction,
eventName: 'click',
element : li
});
});
filter(items) {
let results = [];

items.some(item => {
// add an item if it matches
if (this.itemMatches(item)) {
results.push(item);
}

return (results.length >= this.limit)
});

return results;
}

/**
* returns true when the finding of matching results should continue
*
* @param {*} item
*
* @return {Boolean}
*/
itemMatches(item) {
return this.regex.test(this.label(item));
}

@computedFrom('search')
get regex() {
return new RegExp(this.search, 'gi');
}

/**
Expand All @@ -268,7 +301,7 @@ export class AutoCompleteCustomElement {
return false;
}

return this.search !== '';
return true;
}

/**
Expand Down Expand Up @@ -299,6 +332,8 @@ export class AutoCompleteCustomElement {
where: mergedWhere
};

// only assign limit to query if it is defined. Allows to default to server
// limit when limit bindable is set to falsy value
if (this.limit) {
query.limit = this.limit;
}
Expand Down
34 changes: 19 additions & 15 deletions src/component/bootstrap/autocomplete.html
@@ -1,28 +1,32 @@
<template>
<div class="dropdown open">
<input
class="form-control dropdown-toggle"
value.bind="search & debounce:100"
placeholder="${'Search' | translate}"
type="text"
id="autocompleteDropdown"
data-toggle="dropdown"
autocomplete="off"
aria-haspopup="true"
aria-expanded="true">
</input>

<div class="form-group">
<input
class="form-control dropdown-toggle"
value.bind="search & throttle"
placeholder="${'Search' | translate}"
type="text"
id="dropdownMenu1"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true">
</input>
</div>

<ul class="dropdown-menu" aria-labelledby="dropdownMenu1"
<ul class="dropdown-menu" aria-labelledby="autocompleteDropdown"
show.bind="results.length > 0">

<li show.bind="lastFindPromise">
<i class="fa fa-circle-o-notch fa-spin"></i>
</li>

<template containerless
repeat.for="result of results">
<li
click.delegate="onSelect(result)"
style="cursor:pointer"
class="${result === selected ? 'au-target active' : 'au-target'}"
aria-expanded="true">
<a innerhtml.bind="labelWithMatches(result)">
<a href="javascript: void(0)" innerhtml.bind="labelWithMatches(result)">
</a>
</li>
</template>
Expand Down