Skip to content

Commit 05f4d5d

Browse files
Merge pull request #412 from upfluence/jv/vel-4969/address-inline
Create new address inline with Google place API
2 parents 3c3cd02 + a49e879 commit 05f4d5d

File tree

6 files changed

+222
-5
lines changed

6 files changed

+222
-5
lines changed
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="fx-col fx-gap-px-6 fx-1" ...attributes>
2+
{{#if this.useGoogleAutocomplete}}
3+
<div class="fx-col fx-1 google-autocomplete-input-container">
4+
<OSS::InputContainer
5+
@onChange={{this.onChange}}
6+
@value={{@value.address}}
7+
{{did-insert this.initAutoCompletion}}
8+
data-control-name="address-inline"
9+
/>
10+
</div>
11+
{{else}}
12+
<div class="fx-col fx-1">
13+
<OSS::InputContainer @value={{@value.address}} @onChange={{this.onChange}} data-control-name="address-inline" />
14+
</div>
15+
{{/if}}
16+
</div>
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import Component from '@glimmer/component';
2+
import { action } from '@ember/object';
3+
import { isTesting } from '@embroider/macros';
4+
import { getOwner } from '@ember/application';
5+
6+
import { Loader } from '@googlemaps/js-api-loader';
7+
8+
interface UtilsAddressInlineArgs {
9+
value: ShippingAddress;
10+
useGoogleAutocomplete?: boolean;
11+
onChange(address: any): void;
12+
}
13+
14+
export type ShippingAddress = {
15+
address: string;
16+
resolved_address: {
17+
line_1: string;
18+
zipcode: string;
19+
city: string;
20+
state?: string;
21+
country_code: string;
22+
} | null;
23+
};
24+
25+
type GAddressComponent = google.maps.GeocoderAddressComponent;
26+
type GAutoComplete = google.maps.places.Autocomplete;
27+
type GPlaceResult = google.maps.places.PlaceResult;
28+
29+
export default class extends Component<UtilsAddressInlineArgs> {
30+
get useGoogleAutocomplete(): boolean {
31+
return this.args.useGoogleAutocomplete ?? true;
32+
}
33+
34+
@action
35+
initAutoCompletion(): void {
36+
if (isTesting()) return;
37+
this.appendContainerLocally();
38+
const loader = new Loader({
39+
apiKey: getOwner(this).resolveRegistration('config:environment').google_map_api_key,
40+
version: 'weekly'
41+
});
42+
43+
loader.importLibrary('places').then(({ Autocomplete }) => {
44+
const input = document.querySelector('[data-control-name="address-inline"] input') as HTMLInputElement;
45+
const options = {
46+
fields: ['address_components'],
47+
strictBounds: false,
48+
types: ['address']
49+
};
50+
const autocomplete = new Autocomplete(input, options);
51+
this.initInputListeners(autocomplete, input);
52+
});
53+
}
54+
55+
@action
56+
onChange(value: string): void {
57+
this.args.onChange({ address: value, resolved_address: null });
58+
}
59+
60+
private appendContainerLocally(): void {
61+
const observer = new MutationObserver((mutationList: any) => {
62+
for (const mutation of mutationList) {
63+
if (mutation.type === 'childList') {
64+
const pacContainer = mutation.addedNodes[0];
65+
if (!pacContainer?.classList.contains('pac-container')) return;
66+
67+
document.querySelector('[data-control-name="address-inline"]')?.append(pacContainer);
68+
observer.disconnect();
69+
}
70+
}
71+
});
72+
observer.observe(document.body, { childList: true });
73+
}
74+
75+
private initInputListeners(autocomplete: GAutoComplete, input: HTMLInputElement): void {
76+
autocomplete.addListener('place_changed', () => {
77+
const place = autocomplete.getPlace();
78+
this.updateAddress(place, input);
79+
});
80+
}
81+
82+
private updateAddress(place: GPlaceResult, input: HTMLInputElement): void {
83+
let address1: string = '';
84+
let zipcode: string = '';
85+
let city: string = '';
86+
let state: string = '';
87+
let country: string = '';
88+
89+
const mapper: { [key: string]: (comp: GAddressComponent) => void } = {
90+
street_number: (comp) => {
91+
address1 = `${comp.long_name} ${address1}`;
92+
},
93+
route: (comp) => {
94+
address1 += comp.long_name;
95+
},
96+
postal_code: (comp) => {
97+
zipcode = `${comp.long_name}${zipcode}`;
98+
},
99+
postal_code_suffix: (comp) => {
100+
zipcode = `${zipcode}-${comp.long_name}`;
101+
},
102+
locality: (comp) => {
103+
city = comp.long_name;
104+
},
105+
postal_town: (comp) => {
106+
city = comp.long_name;
107+
},
108+
administrative_area_level_1: (comp) => {
109+
state = comp.long_name ?? '';
110+
},
111+
country: (comp) => {
112+
country = comp.short_name;
113+
}
114+
};
115+
116+
place.address_components!.reverse().map((component) => {
117+
const componentType: string = component.types[0];
118+
119+
mapper[componentType]?.(component);
120+
});
121+
122+
this.args.onChange({
123+
address: input.value,
124+
resolved_address: {
125+
line_1: address1,
126+
zipcode,
127+
city,
128+
state,
129+
country_code: country
130+
}
131+
});
132+
}
133+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@upfluence/ember-upf-utils/components/utils/address-inline';

tests/dummy/app/controllers/application.js

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export default class ApplicationController extends Controller {
1818
countryCode: 'US',
1919
zipcode: '10016'
2020
};
21+
@tracked shippingAddress = { address: '69 Avenue Victor Hugo, Paris, France', resolved_address: null };
2122

2223
constructor() {
2324
super(...arguments);
@@ -39,4 +40,9 @@ export default class ApplicationController extends Controller {
3940
this.selectedColor = color;
4041
this.selectedIcon = icon;
4142
}
43+
44+
@action
45+
onChangeAddress(value) {
46+
this.shippingAddress = value;
47+
}
4248
}
+10-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
<div class='fx-col fx-gap-px-12 margin-px-30'>
1+
<div class="fx-col fx-gap-px-12 margin-px-30">
22
<Utils::AddressForm
33
@address={{this.address}}
44
@usePhoneNumberInput={{false}}
55
@hideNameAttrs={{false}}
66
@useGoogleAutocomplete={{true}}
77
@onChange={{this.onChange}}
8-
@addressKey='line'
8+
@addressKey="line"
99
/>
1010
<Utils::SocialMediaHandle
11-
@handle='nomad.technologies'
12-
@socialNetwork='instagram'
13-
@errorMessage='This is an error'
11+
@handle="nomad.technologies"
12+
@socialNetwork="instagram"
13+
@errorMessage="This is an error"
1414
@onChange={{this.onSocialMediaHandlerChanged}}
1515
@selectorOnly={{false}}
1616
/>
1717
<LogoMaker @icon={{this.selectedIcon}} @color={{this.selectedColor}} @onChange={{this.onLogoChange}} />
18+
<Utils::AddressInline
19+
@value={{this.shippingAddress}}
20+
@onChange={{this.onChangeAddress}}
21+
@useGoogleAutocomplete={{true}}
22+
/>
1823
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { hbs } from 'ember-cli-htmlbars';
2+
import { module, test } from 'qunit';
3+
import { setupRenderingTest } from 'ember-qunit';
4+
import { setupIntl } from 'ember-intl/test-support';
5+
import EmberObject from '@ember/object';
6+
import { render, typeIn } from '@ember/test-helpers';
7+
import sinon from 'sinon';
8+
9+
module('Integration | Component | utils/address-inline', function (hooks) {
10+
setupRenderingTest(hooks);
11+
setupIntl(hooks);
12+
13+
hooks.beforeEach(function () {
14+
this.address = EmberObject.create({
15+
address: '123 Main St',
16+
resolved_address: null
17+
});
18+
this.onChange = sinon.stub();
19+
});
20+
21+
module('when @useGoogleAutocomplete is false', () => {
22+
test('It renders', async function (assert) {
23+
await render(
24+
hbs`<Utils::AddressInline @value={{this.address}} @useGoogleAutocomplete={{false}}
25+
@onChange={{this.onChange}} />`
26+
);
27+
assert.dom('[data-control-name="address-inline"]').exists();
28+
});
29+
30+
test('It renders the correct address', async function (assert) {
31+
await render(
32+
hbs`<Utils::AddressInline @value={{this.address}} @useGoogleAutocomplete={{false}}
33+
@onChange={{this.onChange}} />`
34+
);
35+
assert.dom('[data-control-name="address-inline"] .upf-input').hasValue('123 Main St');
36+
});
37+
38+
test('when type in value, it calls the @onChange', async function (assert) {
39+
await render(
40+
hbs`<Utils::AddressInline @value={{this.address}} @useGoogleAutocomplete={{false}}
41+
@onChange={{this.onChange}} />`
42+
);
43+
await typeIn('[data-control-name="address-inline"] .upf-input', 'reet', { delay: 0 });
44+
assert.equal(this.onChange.callCount, 4);
45+
assert.true(this.onChange.lastCall.calledWith({ address: '123 Main Street', resolved_address: null }));
46+
});
47+
});
48+
49+
test('when @useGoogleAutocomplete is true, it renders the google autocomplete input', async function (assert) {
50+
await render(
51+
hbs`<Utils::AddressInline @value={{this.address}} @useGoogleAutocomplete={{true}}
52+
@onChange={{this.onChange}} />`
53+
);
54+
assert.dom('.google-autocomplete-input-container [data-control-name="address-inline"]').exists();
55+
});
56+
});

0 commit comments

Comments
 (0)