Skip to content

Commit

Permalink
Add vanilla address component and Nova address field.
Browse files Browse the repository at this point in the history
  • Loading branch information
phuclh committed Apr 28, 2021
1 parent 004d379 commit 41779be
Show file tree
Hide file tree
Showing 12 changed files with 559 additions and 202 deletions.
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
],
"require": {
"php": "^7.4|^8.0",
"emilianotisato/nova-google-autocomplete-field": "^0.8.0",
"giggsey/libphonenumber-for-php": "^8.12",
"tipoff/authorization": "^2.8.6",
"tipoff/laravel-google-api": "^2.1.0",
Expand Down
2 changes: 1 addition & 1 deletion dist/js/field.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"vue": "^2.5.0",
"vue-country-flag": "^2.0.4",
"vue-debounce": "^2.6.0",
"vue-tel-input": "^5.2.0"
}
}
164 changes: 164 additions & 0 deletions resources/js/address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const Address = (function () {
const autocompleteParams = {
input: null,
sessionToken: null,
componentRestrictions: {
country: 'us',
},
fields: [
'address_components',
// "place_id", // if needed, Google place ID
// "utc_offset_minutes", // if needed, derive timezone from minutes
],
types: [
'address',
],
};

const placeDetailsParams = {
placeId: null,
sessionToken: null,
fields: [
'address_components',
// can retrieve more fields if needed for data consistency e.g. timezone
],
};

let autocompleteService;
let placesService;
let predictionListElementId;
let placeElementId;

let resetSessionToken = function () {
let sessionToken = new google.maps.places.AutocompleteSessionToken();

autocompleteParams.sessionToken = sessionToken;
placeDetailsParams.sessionToken = sessionToken;
};

window.initGoogleMapAutocomplete = function () {
autocompleteService = new google.maps.places.AutocompleteService();
placesService = new google.maps.places.PlacesService(document.getElementById(placeElementId));

resetSessionToken();
};

let getPredictions = function (query) {
return new Promise((resolve, reject) => {
let startsWithStreetNumber = /^\d/;

if (!startsWithStreetNumber.test(query)) {
hidePredictions();
reject('Please enter a street number.');
} else {
showPredictions();
autocompleteParams.input = query;
autocompleteService.getPlacePredictions(autocompleteParams, (predictions, status) => {
if (status === 'OK') {
resolve(predictions);
}
});
}
})
};

let getAddressInfo = function (placeId) {
return new Promise((resolve, reject) => {
placeDetailsParams.placeId = placeId;
placesService.getDetails(placeDetailsParams, function (placeDetails, status) {
let addressLine1 = '';
let city = '';
let state = '';
let zip = '';

// Get each component of the address from the place details,
// and then fill-in the corresponding field on the form.
// place.address_components are google.maps.GeocoderAddressComponent objects
// which are documented at http://goo.gle/3l5i5Mr
for (const component of placeDetails.address_components) {
// Street number
switch (component.types[0]) {
case 'street_number':
addressLine1 = `${component.long_name} ${addressLine1}`;
break;

// Street name, e.g. Main Street
case 'route':
addressLine1 += component.long_name;
break;

// Postal code
case 'postal_code':
zip = `${component.long_name}${zip}`;
break;

// case "postal_code_suffix":
// postcode = `${postcode}-${component.long_name}`;
// break;

// City
case 'locality':
city = component.long_name;
break;

// State
case 'administrative_area_level_1':
state = component.short_name;
break;

// case "country":
// country = component.long_name;
// break;
}
}

resolve({
addressLine1: addressLine1,
city: city,
state: state,
zip: zip
});
});
resetSessionToken();
});
};

let showPredictions = function () {
document.getElementById(predictionListElementId).classList.remove('invisible');
};

let hidePredictions = function () {
document.getElementById(predictionListElementId).classList.add('invisible');
};

let addGoogleMapScript = function (key) {
let documentTag = document, tag = 'script',
object = documentTag.createElement(tag),
scriptTag = documentTag.getElementsByTagName(tag)[0];

object.src = `https://maps.googleapis.com/maps/api/js?key=${key}&libraries=places&callback=initGoogleMapAutocomplete`;

scriptTag.parentNode.insertBefore(object, scriptTag);
};

let setup = function (settings) {
// Assign essential element.
predictionListElementId = settings.predictionListElementId;
placeElementId = settings.placeElementId;

// Add Google Map script.
addGoogleMapScript(settings.googleApiKey);
};

// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly
return {
setup: setup,
getPredictions: getPredictions,
showPredictions: showPredictions,
hidePredictions: hidePredictions,
getAddressInfo: getAddressInfo
}
})();

export default Address;
108 changes: 108 additions & 0 deletions resources/js/components/Address/FormField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<template>
<default-field :field="field" :errors="errors" :show-help-text="showHelpText">
<template slot="field">
<input
:ref="field.attribute"
:id="field.attribute"
:dusk="field.attribute"
type="text"
v-model="value"
v-debounce:500ms="getPredictions"
@focus="showPredictions"
v-click-outside="hidePredictions"
class="w-full form-control form-input form-input-bordered"
:class="errorClasses"
:placeholder="field.name"
:disabled="isReadonly"
/>
<div id="results-list" class="absolute top-9 z-10 w-full">
<div
v-for="prediction in predictions"
:key="prediction.place_id"
@click="getAddressInfo(prediction.place_id)"
class="block w-full px-2 py-1 text-left bg-white cursor-default hover:bg-gray-50"
>
{{ prediction.description }}
</div>
</div>
<div id="attributions"></div>
</template>
</default-field>
</template>

<script>
import {FormField, HandlesValidationErrors} from 'laravel-nova'
export default {
mixins: [HandlesValidationErrors, FormField],
data() {
return {
placesAutocomplete: null,
predictions: []
}
},
/**
* Mount the component.
*/
mounted() {
this.setInitialValue()
this.field.fill = this.fill
Nova.$on(this.field.attribute + '-value', value => {
this.value = value
})
this.initializePlaces();
},
methods: {
getPredictions() {
this.placesAutocomplete.getPredictions(this.value)
.then(res => {
this.predictions = res;
})
.catch(error => {
this.placesAutocomplete.hidePredictions();
Nova.error(error);
});
},
showPredictions() {
this.placesAutocomplete.showPredictions();
},
hidePredictions() {
this.placesAutocomplete.hidePredictions();
},
getAddressInfo(placeId) {
this.placesAutocomplete.getAddressInfo(placeId)
.then(res => {
this.value = res.addressLine1;
Nova.$emit(this.field.secondAddressLine + '-value', '');
Nova.$emit(this.field.city + '-value', res.city);
Nova.$emit(this.field.postalCode + '-value', res.zip);
Nova.$emit(this.field.state + '-value', res.state);
});
},
/**
* Initialize Algolia places library.
*/
initializePlaces() {
const places = require('./../../address').default;
places.setup({
googleApiKey: Nova.config.googleMapApiKey,
predictionListElementId: 'results-list',
placeElementId: 'attributions'
});
this.placesAutocomplete = places;
}
}
}
</script>
24 changes: 23 additions & 1 deletion resources/js/field.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import VueTelInput from 'vue-tel-input'
import VueTelInput from 'vue-tel-input';
import vueDebounce from 'vue-debounce';

Nova.booting((Vue, router) => {
Vue.use(VueTelInput);
Vue.use(vueDebounce);

Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
el.clickOutsideEvent = function (event) {
// here I check that click was outside the el and his children
if (!(el == event.target || el.contains(event.target))) {
// and if it did, call method provided in attribute value
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent)
},
unbind: function (el) {
document.body.removeEventListener('click', el.clickOutsideEvent)
},
});

// Phone Number field
Vue.component('index-phone-number', require('./components/PhoneNumber/IndexField'));
Vue.component('detail-phone-number', require('./components/PhoneNumber/DetailField'));
Vue.component('form-phone-number', require('./components/PhoneNumber/FormField'));

// Address field
Vue.component('form-address-field', require('./components/Address/FormField'));
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ class="flex justify-between w-full text-gray-700 ring-1 ring-gray-300 rounded-md
name="address-line-1"
type="text"
readonly="readonly"
onfocus="focusField()"
onblur="blurField()"
class="w-full px-2 py-1 focus:outline-none"
required
>
Expand All @@ -25,8 +23,6 @@ class="w-full px-2 py-1 focus:outline-none"
id="address-line-2"
name="address-line-2"
type="text"
onfocus="focusField()"
onblur="blurField()"
class="w-full px-2 py-1 focus:outline-none"
>
</div>
Expand Down Expand Up @@ -54,4 +50,4 @@ class="w-full px-2 py-1 focus:outline-none"
required
>
</div>
</div>
</div>
Loading

0 comments on commit 41779be

Please sign in to comment.