From 66c3175affe59bfb3cf634b6b915339504c2c26b Mon Sep 17 00:00:00 2001 From: "Joseph J. Schmitt" Date: Tue, 6 Jun 2017 17:07:41 -0400 Subject: [PATCH] Add ability to prevent or delay updating the ngModel on choose/unchoose. (#62) `onChosen` and `onUnchosen` now get access to an `$event` object in their locals which let them call `preventDefault()` to prevent updating of the model, and later `performDefault()` to perform it. Also, added unit tests for the `choose` and `unchoose` functions, including these new features. --- README.md | 16 +++- package.json | 2 +- .../ngcOmniboxControllerSpec.js | 87 +++++++++++++++++++ src/angularComponent/ngcOmniboxController.js | 51 +++++++---- 4 files changed, 134 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index bab02f0..719579d 100644 --- a/README.md +++ b/README.md @@ -226,10 +226,18 @@ field receives focus, you can use the `target` property of the event object to f the entire component, not just the field. This blur event also has logic to reduce the noise that sometimes happens where it'll lose focus then immediately regain it, so the blur is called only after a timeout to make sure it doesn't re-receive focus first. -- `onChosen({choice})`: An expression that's called when a suggestion is chosen. In its locals it -has access to `choice`, which is the item that was chosen. -- `onUnchosen({choice})`: An expression that's called when a suggestion is unchosen (removed as a -choice). In its locals it has access to `choice`, which is the item that was unchosen. +- `onChosen({choice, $event})`: An expression that's called when a suggestion is chosen. In its +locals it has access to `choice`, which is the item that was chosen, and an `$event` object. The +`$event` object has the following properties: `isDefaultPrevented`, `preventDefault()`, and +`performDefault()`. If `isDefaultPrevented` is set to true by calling `preventDefault()` from this +callback function, then the choice is not automatically added to the ngModel. If you then do want +the choice to be added, you can call `performDefault()` to do so. +- `onUnchosen({choice, $event})`: An expression that's called when a suggestion is unchosen (removed as a +choice). In its locals it has access to `choice`, which is the item that was unchosen, and an +`$event` object. The `$event` object has the following properties: `isDefaultPrevented`, +`preventDefault()`, and `performDefault()`. If `isDefaultPrevented` is set to true by calling +`preventDefault()` from this callback function, then the choice is not automatically removed from +the ngModel. If you then do want the choice to be removed, you can call `performDefault()` to do so. - `onShowSuggestions({suggestions})`: An expression that's called when the suggestions UI is shown. In its locals it has access to `suggestions`. - `onHideSuggestions({suggestions})`: An expression that's called when the suggestions UI is diff --git a/package.json b/package.json index be68e00..edca884 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngc-omnibox", - "version": "0.3.5", + "version": "0.4.0", "description": "A modern, flexible, Angular 1.x autocomplete library with limited assumptions.", "main": "dist/ngc-omnibox.js", "scripts": { diff --git a/spec/tests/angularComponent/ngcOmniboxControllerSpec.js b/spec/tests/angularComponent/ngcOmniboxControllerSpec.js index 423b8a7..044890c 100644 --- a/spec/tests/angularComponent/ngcOmniboxControllerSpec.js +++ b/spec/tests/angularComponent/ngcOmniboxControllerSpec.js @@ -22,6 +22,8 @@ describe('ngcOmnibox.angularComponent.ngcOmniboxController', () => { omniboxController = new NgcOmniboxController([document], [fakeEl], {$apply() {}}); omniboxController._suggestionElements = [fakeEl, fakeEl, fakeEl, fakeEl]; omniboxController.isSelectable = () => {}; + omniboxController.onChosen = () => {}; + omniboxController.onUnchosen = () => {}; }); it('should inject $document, $element, and $scope', () => { @@ -414,6 +416,91 @@ describe('ngcOmnibox.angularComponent.ngcOmniboxController', () => { }); }); + describe('choosing and unchoosing', () => { + describe('when multiple is off', () => { + beforeEach(() => { + omniboxController.multiple = false; + omniboxController.ngModel = 'one'; + }); + + it('should set the ngModel to the choice when multiple is off', () => { + omniboxController.choose('three'); + expect(omniboxController.ngModel).toEqual('three'); + }); + + it('should set the ngModel to null when unchoosing if multiple is off', () => { + omniboxController.unchoose('one'); + expect(omniboxController.ngModel).toEqual(null); + }); + }); + + describe('when multiple is on', () => { + beforeEach(() => { + omniboxController.multiple = true; + omniboxController.ngModel = ['one', 'two']; + }); + + it('should push the choice to an array', () => { + omniboxController.choose('three'); + expect(omniboxController.ngModel).toEqual(['one', 'two', 'three']); + }); + + it('should not update the ngModel when onChosen event prevents default', () => { + omniboxController.onChosen = ({$event}) => $event.preventDefault(); + omniboxController.choose('three'); + + expect(omniboxController.ngModel).toEqual(['one', 'two']); + }); + + it('should update the ngModel when onChosen event prevents then peforms default', (done) => { + omniboxController.onChosen = ({$event}) => { + $event.preventDefault(); + expect(omniboxController.ngModel).toEqual(['one', 'two']); + + $event.performDefault(); + expect(omniboxController.ngModel).toEqual(['one', 'two', 'three']); + + done(); + }; + + omniboxController.choose('three'); + }); + + it('should not update the ngModel when choosing if an item is not selectable', () => { + omniboxController.isSelectable = () => false; + omniboxController.choose('three'); + + expect(omniboxController.ngModel).toEqual(['one', 'two']); + }); + + it('should remove a choice from the ngModel array when unchoosing', () => { + omniboxController.unchoose('two'); + expect(omniboxController.ngModel).toEqual(['one']); + }); + + it('should not update the ngModel when onUnchosen event prevents default', () => { + omniboxController.onUnchosen = ({$event}) => $event.preventDefault(); + omniboxController.unchoose('two'); + + expect(omniboxController.ngModel).toEqual(['one', 'two']); + }); + + it('should update the ngModel when onUnchosen event prevents then peforms default', (done) => { + omniboxController.onUnchosen = ({$event}) => { + $event.preventDefault(); + expect(omniboxController.ngModel).toEqual(['one', 'two']); + + $event.performDefault(); + expect(omniboxController.ngModel).toEqual(['one']); + + done(); + }; + + omniboxController.unchoose('two'); + }); + }); + }); + describe('choices visibility', () => { beforeEach(() => { omniboxController.multiple = true; diff --git a/src/angularComponent/ngcOmniboxController.js b/src/angularComponent/ngcOmniboxController.js index 4ae4af2..2d60b03 100644 --- a/src/angularComponent/ngcOmniboxController.js +++ b/src/angularComponent/ngcOmniboxController.js @@ -395,18 +395,28 @@ export default class NgcOmniboxController { choose(item, shouldFocusField = true) { if (item && !(Array.isArray(this.ngModel) && this.ngModel.indexOf(item) >= 0) && this.isSelectable({suggestion: item}) !== false) { - if (this.multiple) { - this.ngModel = this.ngModel || []; - this.ngModel.push(item); - } else { - this.ngModel = item; - } - this.onChosen({choice: item}); + const $event = { + isDefaultPrevented: false, + preventDefault: () => $event.isDefaultPrevented = true, + performDefault: () => { + $event.isDefaultPrevented = false; + + if (this.multiple) { + this.ngModel = this.ngModel || []; + this.ngModel.push(item); + } else { + this.ngModel = item; + } - this.query = ''; - shouldFocusField && this.focus(); - this.hideSuggestions = true; + this.query = ''; + shouldFocusField && this.focus(); + this.hideSuggestions = true; + } + }; + + this.onChosen({choice: item, $event}); + !$event.isDefaultPrevented && $event.performDefault(); } } @@ -419,15 +429,22 @@ export default class NgcOmniboxController { */ unchoose(item, shouldFocusField = true) { if (item) { - if (Array.isArray(this.ngModel)) { - this.ngModel.splice(this.ngModel.indexOf(item), 1); - } else if (!this.multiple) { - this.ngModel = null; - } + const $event = { + isDefaultPrevented: false, + preventDefault: () => $event.isDefaultPrevented = true, + performDefault: () => { + if (Array.isArray(this.ngModel)) { + this.ngModel.splice(this.ngModel.indexOf(item), 1); + } else if (!this.multiple) { + this.ngModel = null; + } - this.onUnchosen({choice: item}); + shouldFocusField && this.focus(); + } + }; - shouldFocusField && this.focus(); + this.onUnchosen({choice: item, $event}); + !$event.isDefaultPrevented && $event.performDefault(); } }