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

Commit

Permalink
Merge pull request #2 from bas080/feat/items-bindable
Browse files Browse the repository at this point in the history
feat(project): add items bindable
  • Loading branch information
RWOverdijk committed Jan 11, 2017
2 parents 797ddd1 + aacbcf6 commit c351533
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 68 deletions.
27 changes: 24 additions & 3 deletions README.md
Expand Up @@ -80,12 +80,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 @@ -132,6 +136,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 @@ -66,10 +70,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;

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

Expand Down Expand Up @@ -127,35 +134,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];

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 @@ -203,7 +210,6 @@ export class AutoCompleteCustomElement {
this.results = [];
this.justSelected = true;
this.search = this.label(this.value);
this.showResults = false;
}

/**
Expand All @@ -213,49 +219,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) {
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 @@ -270,7 +303,7 @@ export class AutoCompleteCustomElement {
return false;
}

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

/**
Expand Down Expand Up @@ -301,6 +334,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

0 comments on commit c351533

Please sign in to comment.