Skip to content

Commit

Permalink
ISSUE-105: fix: Clean DOM only when possible (esbanarango#108)
Browse files Browse the repository at this point in the history
* ISSUE-105: fix: Clean DOM only when possible

* Only remove Google elements when there are not more active components. I have to
  review if there might be race conditions when adding or removing components.
* Add service to manage Google autocomplete instances global state.
* Update Mocha and update tests.

* Force build

* Force build in Travis beta
  • Loading branch information
dmuneras authored May 3, 2020
1 parent ed32d6f commit 6cd8a92
Show file tree
Hide file tree
Showing 11 changed files with 1,207 additions and 72 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@
/package.json.ember-try

#nvm
.nvmrc
.nvmrc

# VSCODE

.vscode/
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ language: node_js
node_js:
# we recommend testing addons with the same minimum supported node version as Ember CLI
# so that your addon works for all apps
- "8"
- "10"

sudo: false
dist: trusty
Expand Down
41 changes: 22 additions & 19 deletions addon/components/place-autocomplete-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ import {
isBlank
} from '@ember/utils';
import { scheduleOnce, run } from "@ember/runloop";
import { inject as service } from '@ember/service';

export default Component.extend({
/* SERVICES
---------------------------------------------------------------------------*/
placeAutocompleteManagerService: service('google-place-autocomplete/manager'),


/* HOOKS
---------------------------------------------------------------------------*/
/**
* Set default values in component init
*/
Expand All @@ -25,6 +33,20 @@ export default Component.extend({
// the data attributes
this._bindDataAttributesToInput();
scheduleOnce('afterRender', this, 'setupComponent');

this.get('placeAutocompleteManagerService').register();
},

willDestroy() {
if (isPresent(this.autocomplete)) {
let google = this.google || ((window) ? window.google : null);
this.get('placeAutocompleteManagerService').register();

if(google && google.maps && google.maps.event) {
google.maps.event.clearInstanceListeners(this.autocomplete);
this.get('placeAutocompleteManagerService').removePlacesAutoCompleteContainersIfRequired();
}
}
},

/**
Expand Down Expand Up @@ -90,18 +112,6 @@ export default Component.extend({
}
},

willDestroy() {
if (isPresent(this.autocomplete)) {
let google = this.google || ((window) ? window.google : null);
if(google && google.maps && google.maps.event) {
google.maps.event.clearInstanceListeners(this.autocomplete);

// remove googles autocomplete drop down containers from the dom
this._removePlacesAutoCompleteContainers();
}
}
},

setAutocomplete() {
if (isEmpty(this.autocomplete)) {
const inputElement = document.getElementById(this.elementId).getElementsByTagName('input')[0],
Expand Down Expand Up @@ -196,13 +206,6 @@ export default Component.extend({
properties.forEach((property) => input.setAttribute(property, this.get(property)));
},

_removePlacesAutoCompleteContainers() {
const pacContainers = document.querySelectorAll('.pac-container');
for (let i = 0; pacContainers.length > i; i++) {
pacContainers[i].parentNode.removeChild(pacContainers[i]);
}
},

actions: {
onBlur() {
this._callCallback('onBlurCallback');
Expand Down
47 changes: 47 additions & 0 deletions addon/services/google-place-autocomplete/manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Service from '@ember/service';

export default Service.extend({
init() {
this._super(...arguments);

this.set('numberOfActiveAutoCompleteFields', 0);
},

/**
* @description Increments the counter of active components.
* Intended to be used everytime a new place-autocomplete-filed is
* instanciated.
*/
register() {
this.incrementProperty('numberOfActiveAutoCompleteFields');
},

/**
* @description Decrements the counter of active components.
* Intended to be used everytime a new place-autocomplete-filed is
* going to be destroyed.
*/
unregister() {
this.decrementProperty('numberOfActiveAutoCompleteFields');
},

/**
* @description Cleanup DOM when ALL component instances of place-autocomplete-field
* are removed from the DOM. If there are still components active, it does nothing.
*
* @returns { Boolean } - Indicates whether the DOM was cleaned or not.
*/
removePlacesAutoCompleteContainersIfRequired() {
if (!document || this.numberOfActiveAutoCompleteFields > 0) {
return false;
}

const pacContainers = document.querySelectorAll('.pac-container');

for (let index = 0; pacContainers.length > index; index++) {
pacContainers[index].parentNode.removeChild(pacContainers[index]);
}

return true;
},
});
1 change: 1 addition & 0 deletions app/services/google-place-autocomplete/manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-place-autocomplete/services/google-place-autocomplete/manager';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.1.0",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-mocha": "^0.14.0",
"ember-mocha": "0.16.0",
"ember-native-dom-helpers": "^0.5.10",
"ember-power-select": "^2.2.2",
"ember-prism": "^0.4.0",
Expand Down
2 changes: 2 additions & 0 deletions tests/test-helper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Application from '../app';
import config from '../config/environment';
import { setApplication } from '@ember/test-helpers';
import { start } from 'ember-mocha';

setApplication(Application.create(config.APP));
start();
18 changes: 9 additions & 9 deletions tests/unit/components/place-autocomplete-field-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,52 @@ describe('Unit | Component | PlaceAutocompleteField', function() {
setupTest('component:place-autocomplete-field');

it('returns empty array on undefined/null', function(){
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
expect(component._typesToArray()).to.eql([]);

component.set('types', null);
expect(component._typesToArray()).to.eql([]);
});

it('converts types option to array', function(){
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
component.set('types', 'geocode');
expect(component._typesToArray()).to.eql(['geocode']);
});

it('converts types option to array more two elements', function(){
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
component.set('types', 'geocode,establishment');
expect(component._typesToArray()).to.eql(['geocode','establishment']);
});

it('converts types in an empty string', function(){
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
component.set('types', '');
expect(component._typesToArray()).to.eql([]);
});

it('supports array passed as types option', function() {
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
component.set('types', ['geocode', 'establishment']);
expect(component._typesToArray()).to.eql(['geocode', 'establishment']);
});

it('converts fields option to array', function(){
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
component.set('fields', 'place_id,name,types');
expect(component._fieldsToArray()).to.eql(['place_id', 'name', 'types']);
});

it('populates the fields option when placeIdOnly is specified', function(){
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
component.set('placeIdOnly', true);
const options = component.getOptions();
expect(options.fields).to.eql(['place_id', 'name', 'types']);
});

it('get geolocate is not available', function(){
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
let navigator = {
geolocation: false
};
Expand All @@ -67,7 +67,7 @@ describe('Unit | Component | PlaceAutocompleteField', function() {
});

it('get geolocate is available', function() {
let component = this.container.owner.lookup('component:place-autocomplete-field');
let component = this.owner.lookup('component:place-autocomplete-field');
let google = {};
google.maps = {
Circle(center, radio) {
Expand Down
31 changes: 17 additions & 14 deletions tests/unit/services/google-place-autocomplete-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,41 @@ describe('Unit | Service | google place autocomplete', function() {

context('updateSessionToken', function() {
it('updates service session token', function() {
let service = this.subject();
let firstSessionToken = service.get('sessionToken');
const service = this.owner.factoryFor('service:google-place-autocomplete').create();
const { sessionToken: firstSessionToken } = service;

service.updateSessionToken();
expect(firstSessionToken.Pf).not.to.equal(service.get('sessionToken').Pf);

expect(firstSessionToken.Pf).not.to.equal(service.sessionToken.Pf);
});
});


context('getDetails', function() {
it('refresh session token after requesting place details', function() {
let service = this.subject();
let firstSessionToken = service.get('sessionToken');
let request = {
it('refresh session token after requesting place details', async function() {
const service = this.owner.factoryFor('service:google-place-autocomplete').create();
const { sessionToken: firstSessionToken } = service;
const request = {
placeId: 'ALWAYS_RETURN_THE_SAME',
fields: ['address_components', 'formatted_address', 'place_id', 'rating']
};

return service.getDetails(request).then(() => {
expect(firstSessionToken.Pf).not.to.equal(service.get('sessionToken').Pf);
});
await service.getDetails(request);

expect(firstSessionToken.Pf).not.to.equal(service.sessionToken.Pf);
});
});

context('getPlacePredictions', function() {
context('when placeDetails is not called', function() {
it('never updates sessionToken', function() {
let service = this.subject();
let sessionToken = service.get('sessionToken');
let i = 0;
const service = this.owner.factoryFor('service:google-place-autocomplete').create();
const { sessionToken } = service;
let i = 0;

while(i < 5) {
service.getPlacePredictions({ input: 'El eslabón prendido' });
expect(sessionToken.Pf).to.equal(service.get('sessionToken').Pf);
expect(sessionToken.Pf).to.equal(service.sessionToken.Pf);
i += 1;
}
});
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/services/google-place-autocomplete/manager-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupTest } from 'ember-mocha';

describe('Unit | Service | google-place-autocomplete/manager', function() {
setupTest();

// Replace this with your real tests.
it('exists', function() {
let service = this.owner.lookup('service:google-place-autocomplete/manager');
expect(service).to.be.ok;
});
});
Loading

0 comments on commit 6cd8a92

Please sign in to comment.