diff --git a/module/typeahead.js b/module/typeahead.js index 25684f0..5a9b811 100644 --- a/module/typeahead.js +++ b/module/typeahead.js @@ -191,10 +191,25 @@ export class Typeahead { }, 'update': (event) => { + // To prevent multiple key presses triggering an update we + // apply a small delay before calling the fetch via the + // `delayedFetch` function. + // Get the query string const {behaviours} = this.constructor const q = behaviours.query[this._behaviours.query](this) - this.update(q) + + const delayedUpdate = () => { + this.update(q) + } + + // Cancel any existing call to delayed update + if (this._delayedUpdate) { + clearTimeout(this._delayedUpdate) + } + + // Schedule a call to delayed update + this._delayedUpdate = setTimeout(delayedUpdate, 100) } } } @@ -293,6 +308,15 @@ export class Typeahead { } ) + // Cancel any delayed update or fetch + if (this._delayedUpdate) { + clearTimeout(this._delayedUpdate) + } + + if (this._signal) { + this._signal.cancelled = true + } + if (this.typeahead) { $.listen( this.typeahead, @@ -383,7 +407,6 @@ export class Typeahead { 'keydown': this._handlers.nav } ) - } /** @@ -409,7 +432,7 @@ export class Typeahead { /** * Open the typeahead. */ - open() { + open() { // If the `autoFirst` option is true and no suggestion currently has // focus then select the first option. if (this._options.autoFirst && this.index === -1) { @@ -521,9 +544,23 @@ export class Typeahead { return null } + // Cancel any existing fetch + if (this._signal) { + this._signal.cancelled = true + } + + // Create a new signal for the new request + this._signal = {'cancelled': false} + // Fetch the list of suggestions - return behaviours.fetch[this._behaviours.fetch](this, q) - .then((rawSuggestions) => { + return behaviours.fetch[this._behaviours.fetch](this, q, this._signal) + .then((response) => { + const [rawSuggestions, signal] = response + + // If the signal was cancelled ignore the response + if (signal.cancelled) { + return + } // If no suggestions were returned then close the typeahead // and we're done. @@ -667,7 +704,7 @@ Typeahead.behaviours = { /** * Fetch the suggestions using an AJAX call. */ - 'ajax': (inst, q) => { + 'ajax': (inst, q, signal) => { // Check for cached suggestions const cacheKey = q.substr(0, inst._options.minChars).toLowerCase() @@ -698,16 +735,16 @@ Typeahead.behaviours = { if (!inst._options.disableCache) { inst._cache[cacheKey] = json.payload.suggestions } - return json.payload.suggestions + return [json.payload.suggestions, signal] }) }, /** * Return the list option (which should be an array) */ - 'array': (inst, q) => { + 'array': (inst, q, signal) => { return new Promise((resolve, reject) => { - resolve(inst._options.list) + resolve([inst._options.list, signal]) }) }, @@ -715,7 +752,7 @@ Typeahead.behaviours = { * Select a list of DOM element using the list option as a CSS * selector and return the content of the elements as suggestions. */ - 'elements': (inst, q) => { + 'elements': (inst, q, signal) => { return new Promise((resolve, reject) => { const elms = $.many(inst._options.list) @@ -726,26 +763,25 @@ Typeahead.behaviours = { 'value': content } }) - resolve(suggestions) - + resolve([suggestions, signal]) }) }, /** * Parse the list as a JSON string and return the result. */ - 'json': (inst, q) => { + 'json': (inst, q, signal) => { return new Promise((resolve, reject) => { - resolve(JSON.parse(inst._options.list)) + resolve([JSON.parse(inst._options.list), signal]) }) }, /** * Split the list option (which should be a comma separated string). */ - 'string': (inst, q) => { + 'string': (inst, q, signal) => { return new Promise((resolve, reject) => { - resolve(inst._options.list.split(',')) + resolve([inst._options.list.split(','), signal]) }) } diff --git a/package.json b/package.json index 4469975..3704f67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "manhattan-typeahead", - "version": "1.0.5", + "version": "1.0.6", "description": "Type-a-head and tokens for form fields.", "engines": { "node": ">=8.9.4" diff --git a/spec/typeahead.spec.js b/spec/typeahead.spec.js index 2bc0289..8756093 100644 --- a/spec/typeahead.spec.js +++ b/spec/typeahead.spec.js @@ -730,6 +730,33 @@ describe('Typeahead', () => { otherTypeahead.close.should.have.been.called }) }) + + describe('cancel previous call', () => { + + beforeEach(() => { + sinon.spy(Typeahead.behaviours.fetch, 'array') + + }) + + afterEach(() => { + Typeahead.behaviours.fetch['array'].restore() + }) + + it('should prevent the results of the initial response ' + + 'being applied', async () => { + + otherTypeahead.update('fo') + await otherTypeahead.update('fo') + + const arrayFunc = Typeahead.behaviours.fetch['array'] + arrayFunc.returnValues[0].then((response) => { + response[1].cancelled.should.be.true + }) + arrayFunc.returnValues[1].then((response) => { + response[1].cancelled.should.be.false + }) + }) + }) }) }) @@ -789,6 +816,7 @@ describe('Typeahead', () => { sinon.spy(typeahead, 'next') sinon.spy(typeahead, 'previous') sinon.spy(typeahead, 'select') + sinon.spy(typeahead, 'update') }) afterEach(() => { @@ -796,6 +824,7 @@ describe('Typeahead', () => { typeahead.next.restore() typeahead.previous.restore() typeahead.select.restore() + typeahead.update.restore() typeahead.destroy() }) @@ -946,10 +975,29 @@ describe('Typeahead', () => { }) }) + + describe('update', () => { + + it('should call the update method only after a short ' + + 'delay', (done) => { + + $.dispatch(typeahead.input, 'input') + $.dispatch(typeahead.input, 'input') + typeahead.update.should.not.have.been.called + + const afterDelay = () => { + typeahead.update.should.have.been.called + done() + } + + setTimeout(afterDelay, 200) + }) + }) }) describe('behaviours > coerce', () => { const behaviours = Typeahead.behaviours.coerce + let signal = null let typeahead = null beforeEach(() => { @@ -1023,8 +1071,13 @@ describe('Typeahead', () => { describe('behaviours > fetch', () => { const behaviours = Typeahead.behaviours.fetch + let signal = null let typeahead = null + beforeEach(() => { + signal = {'cancelled': true} + }) + afterEach(() => { if (typeahead) { typeahead.destroy() @@ -1068,16 +1121,23 @@ describe('Typeahead', () => { typeahead.init() // With no args - const suggestions = await behaviours.ajax(typeahead, 'fo') + const suggestions = await behaviours.ajax( + typeahead, + 'fo', + signal + ) suggestions.should.deep.equal([ - { - 'label': 'foo', - 'value': 'foo' - }, - { - 'label': 'foobar', - 'value': 'foobar' - } + [ + { + 'label': 'foo', + 'value': 'foo' + }, + { + 'label': 'foobar', + 'value': 'foobar' + } + ], + signal ]) }) @@ -1091,16 +1151,23 @@ describe('Typeahead', () => { typeahead.init() // With args - const suggestions = await behaviours.ajax(typeahead, 'fo') + const suggestions = await behaviours.ajax( + typeahead, + 'fo', + signal + ) suggestions.should.deep.equal([ - { - 'label': 'foo', - 'value': 'foo' - }, - { - 'label': 'foobar', - 'value': 'foobar' - } + [ + { + 'label': 'foo', + 'value': 'foo' + }, + { + 'label': 'foobar', + 'value': 'foobar' + } + ], + signal ]) global.fetch @@ -1119,8 +1186,8 @@ describe('Typeahead', () => { ) typeahead.init() - await behaviours.ajax(typeahead, 'fo') - await behaviours.ajax(typeahead, 'fo') + await behaviours.ajax(typeahead, 'fo', signal) + await behaviours.ajax(typeahead, 'fo', signal) global.fetch.should.have.been.calledOnce }) @@ -1137,8 +1204,8 @@ describe('Typeahead', () => { ) typeahead.init() - await behaviours.ajax(typeahead, 'fo') - await behaviours.ajax(typeahead, 'fo') + await behaviours.ajax(typeahead, 'fo', signal) + await behaviours.ajax(typeahead, 'fo', signal) global.fetch.should.have.been.calledTwice @@ -1166,16 +1233,23 @@ describe('Typeahead', () => { ) typeahead.init() - const suggestions = await behaviours.array(typeahead, 'fo') + const suggestions = await behaviours.array( + typeahead, + 'fo', + signal + ) suggestions.should.deep.equal([ - { - 'label': 'foo', - 'value': 'foo' - }, - { - 'label': 'foobar', - 'value': 'foobar' - } + [ + { + 'label': 'foo', + 'value': 'foo' + }, + { + 'label': 'foobar', + 'value': 'foobar' + } + ], + signal ]) }) }) @@ -1209,16 +1283,23 @@ describe('Typeahead', () => { ) typeahead.init() - const suggestions = await behaviours.elements(typeahead, 'fo') + const suggestions = await behaviours.elements( + typeahead, + 'fo', + signal + ) suggestions.should.deep.equal([ - { - 'label': 'foo', - 'value': 'foo' - }, - { - 'label': 'foobar', - 'value': 'foobar' - } + [ + { + 'label': 'foo', + 'value': 'foo' + }, + { + 'label': 'foobar', + 'value': 'foobar' + } + ], + signal ]) }) }) @@ -1244,16 +1325,23 @@ describe('Typeahead', () => { ) typeahead.init() - const suggestions = await behaviours.json(typeahead, 'fo') + const suggestions = await behaviours.json( + typeahead, + 'fo', + signal + ) suggestions.should.deep.equal([ - { - 'label': 'foo', - 'value': 'foo' - }, - { - 'label': 'foobar', - 'value': 'foobar' - } + [ + { + 'label': 'foo', + 'value': 'foo' + }, + { + 'label': 'foobar', + 'value': 'foobar' + } + ], + signal ]) }) }) @@ -1268,8 +1356,12 @@ describe('Typeahead', () => { ) typeahead.init() - const suggestions = await behaviours.string(typeahead, 'fo') - suggestions.should.deep.equal(['foo', 'foobar']) + const suggestions = await behaviours.string( + typeahead, + 'fo', + signal + ) + suggestions.should.deep.equal([['foo', 'foobar'], signal]) }) }) })