react-google-autocomplete only works after refresh - google-maps

I made a "Profile" page to show address, zip code, latitude and longitude when user enters(and select) an address by using react-google-autocomplete module.
When a user starts to enter address name, some full addresses are displayed down below.
After selecting one of them, I can get enough information about the address.
But the problem is that it doesn't work at first but works after refreshing the page.
Again...
I open localhost:3000 and move to localhost:3000/profile page by clicking a navbar button.
Then it doesn't work with this error "Google maps places API must be loaded."
And after refreshing the page(F5) it works properly.
I can't find a way to solve this error. Please help me!
//////////////////////
-Parent
<Grid item xs={12} sm={12}>
<MapInput form={form} setForm={setForm} />
</Grid>
///////////////////////
import { usePlacesWidget } from "react-google-autocomplete";
import React from "react";
export default ({form, setForm}) => {
const { ref } = usePlacesWidget({
apiKey: process.env.REACT_APP_GOOGLE_API_KEY,
onPlaceSelected: (place) => {
const address = place.formatted_address;
let city, state, country, zipcode;
for (let i = 0; i < place.address_components.length; i++) {
for (let j = 0; j < place.address_components[i].types.length; j++) {
switch (place.address_components[i].types[j]) {
case "locality":
city = place.address_components[i].long_name;
break;
case "administrative_area_level_1":
state = place.address_components[i].long_name;
break;
case "country":
country = place.address_components[i].long_name;
break;
case "postal_code":
zipcode = place.address_components[i].long_name;
break;
}
}
}
setForm({...form, s_address: address, s_city: city, s_state: state, s_country: country, s_zipcode: zipcode, lat: place.geometry.location.lat(), lng: place.geometry.location.lng()});
},
options: {
types: ["(regions)"],
// componentRestrictions: { country: "ru" },
},
});
return <input ref={ref} style={{
width: "100%",
padding: "18.5px 14px",
margin: "20px 0px",
font: "inherit",
color: "grey"
}} defaultValue={form?.s_address} />;
};

Related

How to pass user entered input on homepage card to a a card created from Drive onItemsSelectedTrigger?

I'm creating a google workspace addon that applies entered text to user selected templates with placeholders. My issue is after I enter the information on the card created by homepageTrigger, if I then select a file/folder on Drive, it creates the new card without any of the entered text. Likewise, if I select an item to enter information then select a new file/folder, the input doesn't carry over. This is a problem in the event that the user accidentally selects the wrong file, enters all the information, then attempts to select the correct file.
My first attempt at a solution was to create an object of currently entered information and pull the value from each key upon reloading.
//formatting for text input = {title : fieldname}
//both title and fieldname are required
let textFields = {
'Date': 'date',
'Name':'name',
'Street Address':'address1',
'City, State Zip':'address2',
'Phone Number':'phoneNum',
'SSN':'ssn',
'E-mail Address':'email',
}
//create object of empty placeholders for template
//pulled from textFields
let enteredInfo = {}
Object.values(textFields).forEach(fieldName => {
enteredInfo[fieldName] = '';
});
The object is created in a global scope, outside of any function and is updated with an onChange function for each text input box using the saveInput function
function saveInput(e) {
for (let [field, value] of Object.entries(e.formInput)) {
if (enteredInfo.hasOwnProperty(field) && field != 'date' && field != '') {
enteredInfo[field] = value
}
}
return CardService.newNavigation().updateCard(buildCard(e));
}
This has the unfortunate effect of having to update the card upon each input, which makes tabbing between input boxes impossible. Unfortunately, while this does work as intended for the current card, when selecting a new drive file, the enteredInfo object displays
enteredInfo[fieldName]:''
The second attempt, which would be the more ideal solution, was to pass the event object with e.formInput(this will be changed before deployment to e.commonEventObject.formInputs as the former is deprecated per documentation), and set the value for each input from that and update the original build card upon selecting an item. The code was as follows:
for (let [title, fieldName] of Object.entries(textFields)) {
if ([title] != 'Date') {
form.addWidget(CardService.newTextInput()
.setFieldName(fieldName)
.setTitle(title)
.setValue(e['formInput'][fieldName])
.setOnChangeAction(save));
}
}
The code together under my cardBuilder.gs page is:
let onDriveHomePageOpen = (e) => buildCard(e);
let onDriveItemsSelected = (e) => itemSelectedCard(e);
//formatting for text input = {title : fieldname}
//both title and fieldname are required
let textFields = {
'Date': 'date',
'Name':'name',
'Street Address':'address1',
'City, State Zip':'address2',
'Phone Number':'phoneNum',
'SSN':'ssn',
'E-mail Address':'email',
}
//create object of empty placeholders for template
//pulled from textFields
let enteredInfo = {}
Object.values(textFields).forEach(fieldName => {
enteredInfo[fieldName] = '';
});
function buildCard(e) {
let header = CardService.newCardHeader()
// .setTitle('Select folder or individual templates')
.setTitle(JSON.stringify(enteredInfo))
let apply = CardService.newAction()
.setFunctionName('applyInfo');
let applyButton = CardService.newTextButton()
.setText('Apply')
.setOnClickAction(apply);
let footer = CardService.newFixedFooter()
.setPrimaryButton(applyButton);
//change timezone to EDT
let currentDate = Date.now() - (5 * 3600000);
let date = CardService.newDatePicker()
.setFieldName('date')
.setValueInMsSinceEpoch(currentDate);
let form = CardService.newCardSection()
.setHeader('Enter Client Information')
.addWidget(date)
let save = CardService.newAction().setFunctionName('saveInput')
for (let [title, fieldName] of Object.entries(textFields)) {
if ([title] != 'Date') {
form.addWidget(CardService.newTextInput()
.setFieldName(fieldName)
.setTitle(title)
.setValue(enteredInfo[fieldName])
.setOnChangeAction(save));
}
}
let card = CardService.newCardBuilder()
.setFixedFooter(footer)
.setHeader(header)
.addSection(form)
.build();
return card;
}
function itemSelectedCard(e) {
let header = CardService.newCardSection();
header.setHeader('Selected Files')
.setCollapsible(true)
.setNumUncollapsibleWidgets(1)
let selectedFiles = []
e.drive.selectedItems.forEach (item => {
selectedFiles.push(item)
});
for (let i = 0; i < selectedFiles.length; i++) {
header.addWidget(CardService.newDecoratedText()
.setText(selectedFiles[i].title)
)}
let apply = CardService.newAction()
.setFunctionName('applyInfo');
let applyButton = CardService.newTextButton()
.setText('Apply')
.setOnClickAction(apply);
let footer = CardService.newFixedFooter()
.setPrimaryButton(applyButton);
//change timezone to EDT
let currentDate = Date.now() - (5 * 3600000);
let date = CardService.newDatePicker()
.setFieldName('date')
.setValueInMsSinceEpoch(currentDate);
let form = CardService.newCardSection()
.setHeader(JSON.stringify(e))
.addWidget(date)
let save = CardService.newAction().setFunctionName('saveInput')
for (let [title, fieldName] of Object.entries(textFields)) {
if ([title] != 'Date') {
form.addWidget(CardService.newTextInput()
.setFieldName(fieldName)
.setTitle(title)
.setValue(enteredInfo[fieldName])
.setOnChangeAction(save));
}
}
let card = CardService.newCardBuilder()
.setFixedFooter(footer)
.addSection(header)
.addSection(form)
.build();
return CardService.newNavigation().updateCard(buildCard(e))
}
Manifest File:
{
"timeZone": "America/New_York",
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"addOns": {
"common": {
"logoUrl": "https://storage.googleapis.com/jplogo/format_ink_highlighter_FILL1_wght200_GRAD0_opsz48.png",
"name": "Doc Merge",
"openLinkUrlPrefixes": [
"https://www.googleapis.com/auth/script.storage",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.addons.metadata.readonly"
]
},
"drive": {
"homepageTrigger": {
"runFunction": "onDriveHomePageOpen",
"enabled": true
},
"onItemsSelectedTrigger": {
"runFunction": "onDriveItemsSelected"
}
},
"sheets": {
"homepageTrigger": {
"runFunction": "onSpreadsheet"
}
}
}
}
I'm a long time lurker but first time poster so I apologize if any of my SO formatting is off. I'd be happy to correct any issues. Thank you for any help you can provide!

How to restrict Google Maps search results to only one country properly?

I am developing an application, in Ionic, where you can plan a trip with a start address and and end address. However I want to limit this feature to only one country. Before writing I have been searching for solutions on the internet, but none of them worked for me.
Have tried these suggestions:
https://stackoverflow.com/a/8282093/8130808
https://stackoverflow.com/a/10170421/8130808
Here is how I have tried to approach it:
//Places markers and displays a route, so the user can accept the current placing
newRoutePlaceMarks(place: any): void {
var googleDiplay = new google.maps.DirectionsRenderer({ draggable: true });
var route = this.directionsDisplay;
//A bit of a hack, sadly Typescript and google maps arent the best of buddies
this.directionsService.route({
origin: this.routeStart,
destination: this.routeEnd,
travelMode: 'DRIVING',
}, function (response, status) {
if (status === 'OK') {
console.log("status is OK trying to put directions up");
//The reason why I've set the addListener before the actual route is so it gets triggered
//on the creation of the route. Had some problem with figuring out how to actually handle
//the data when on the route creation, as this response function is in strict mode, and outside
google.maps.event.addListener(route, "directions_changed", function () {
console.log("Route changed");
this.global = ShareService.getInstance();
this.directions = route.getDirections();
this.metersToDist = this.directions.routes[0].legs[0].distance.value;
this.timeToDist = this.directions.routes[0].legs[0].duration.value;
this.startAddress = this.directions.routes[0].legs[0].start_address;
this.startCord = this.directions.routes[0].legs[0].start_location;
this.endAddress = this.directions.routes[0].legs[0].end_address;
this.endCord = this.directions.routes[0].legs[0].end_location;
this.global.setMetersToDist(this.metersToDist);
this.global.setTimeToDist(this.timeToDist);
this.global.setStartAddress(this.startAddress);
this.global.setStartCord(this.startCord);
this.global.setEndAddress(this.endAddress);
this.global.setEndCord(this.endCord);
var options = {
types: ['geocode'],
componentRestrictions: { country: "dk" }
};
google.maps.places.Autocomplete(this.startAddress, options);
});
//The actual initialiser for the route
route.setDirections(response);
} else {
alert('Could not display route ' + status);
}
});
}
My problem is that the input is HTTPELEMENT, I get the input from an alert dialog
newRouteInput() {
let alert = this.alertCtrl.create({
title: 'New route',
inputs: [
{
name: 'routeStart',
placeholder: 'Start of route'
},
{
name: 'routeEnd',
placeholder: 'End of route'
}
],
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: data => {
console.log('Cancel clicked');
}
},
{
text: 'Debug start and end',
handler: data => {
if (data.username !== "undefined") {
console.log(data.routeStart + " " + data.routeEnd);
this.routeStart = "Brøndby Strand";
this.routeEnd = "Hvidovre";
this.newRoutePlaceMarks(this.map);
this.routeButton = false;
} else {
return false;
}
}
},
{
text: 'Place route markers',
handler: data => {
if (data.username !== "undefined") {
this.routeStart = data.routeStart;
this.routeEnd = data.routeEnd;
this.newRoutePlaceMarks(this.map);
this.routeButton = false;
} else {
console.log(data.routeStart + " " + data.routeEnd);
return false;
}
}
}
]
});
alert.present();
}
When I run this I get an error because of this.startAddress. It's not null, it contains the start address:
InvalidValueError: not an instance of HTMLInputElement

ReactJS props updating at different speeds in same component

I have a Google maps component in a React/Redux app. When you click an item from a list, it passes down an array of coordinates to render as directions from the user's current location. The props are being passed fine through react-redux mapStateToProps. I'm calling a function to generate the the polyline, this is where my problem is. The marker is generated fine inside of render, but the directions do not render until another entry is clicked. Basically it's always one step behind the current markers. So, for 2 stops, I'll have directions from current location to stop 1, but not stop 2. For 3 stops, current location to stop 1 to stop 2 will be generated, but not stop 3.
When I log out the length of the array of stops inside of render I get the expected amount, a length of 1 for 1 stop. I have tried putting the method inside of componentWillWillReceiveProps and componentWillUpdate, and both methods will log a 0 for 1 stop.
Here's the component, if relevant:
const GoogleMapComponent = React.createClass({
mixin: [PureRenderMixin],
getInitialState: function() {
return {
map: null,
maps: null,
color: 0
}
},
componentWillUpdate: function() {
console.log('LOGS ZERO HERE', this.props.tourList.length)
if (this.state.maps) {
this.calculateAndDisplayRoute(this.state.directionsService, this.state.directionsDisplay, this.props.tourList);
}
},
saveMapReferences: function(map, maps) {
let directionsDisplay = new maps.DirectionsRenderer({map, polylineOptions: {strokeColor: '#76FF03'}, suppressMarkers: true});
let directionsService = new maps.DirectionsService();
this.setState({ map, maps, directionsService, directionsDisplay });
},
generateWaypoints: function(coords) {
return coords.map((coord) => {
return { location: new this.state.maps.LatLng(coord.lat, coord.lng) };
});
},
calculateAndDisplayRoute: function(directionsService, directionsDisplay, tourStops) {
let origin = this.props.userLocation || { lat: 37.77, lng: -122.447 };
let destination = tourStops[tourStops.length - 1];
let directions = { origin, destination, travelMode: this.state.maps.TravelMode.DRIVING };
if (this.props.tourList.length > 1) {
directions.waypoints = this.generateWaypoints(tourStops);
}
if (tourStops.length > 0) {
directionsService.route(directions, (response, status) => {
if (status === this.state.maps.DirectionsStatus.OK) {
directionsDisplay.setDirections(response);
} else {
console.log('Directions request failed due to ' + status);
}
});
} else {
directionsDisplay.set('directions', null);
}
},
render: function() {
console.log('LOGS 1 HERE', this.props.tourList.length)
let markers = this.props.tourList.map((marker, idx) => {
let loc = marker.prevLoc ? marker.prevLoc : 'your current location.';
return <Marker className='point' key={idx} image={marker.poster} lat={marker.lat} lng={marker.lng} location={marker.location} price={marker.price} loc={loc} />
});
let defaultCenter = {lat: 37.762, lng: -122.4394};
let defaultZoom = 12
if (this.props.userLocation !== null) {
return (
<div className='map'>
<GoogleMap defaultCenter={defaultCenter} defaultZoom={defaultZoom} yesIWantToUseGoogleMapApiInternals={true}
onGoogleApiLoaded={({map, maps}) => {
map.setOptions({styles: mapStyles});
this.saveMapReferences(map, maps);
}} >
{markers}
<UserMarker lat={this.props.userLocation.lat} lng= {this.props.userLocation.lng} />
</GoogleMap>
</div>
);
}
return (
<div className='map'>
<GoogleMap defaultCenter={defaultCenter} defaultZoom={defaultZoom} yesIWantToUseGoogleMapApiInternals={true}
onGoogleApiLoaded={({map, maps}) => {
map.setOptions({styles: mapStyles});
this.saveMapReferences(map, maps);
}} >
{markers}
</GoogleMap>
</div>
);
}
});
function mapStateToProps(state) {
return {
tourList: state.sidebar.tourList,
userLocation: state.home.userLocation
}
}
export default connect(mapStateToProps)(GoogleMapComponent);
Figured it out, I was not passing nextProps to componentWillUpdate, so the function was always being called with the old props.
componentWillUpdate is called prior to this.props being updated. Change componentWillUpdate as follows:
componentWillUpdate: function(nextProps) {
console.log('SHOULD LOG ONE HERE', nextProps.tourList.length)
if (this.state.maps) {
this.calculateAndDisplayRoute(this.state.directionsService, this.state.directionsDisplay, nextProps.tourList);
}
}

Dividing a sorted list

I have a list of movies and need to group them in both c# (or angular is also acceptable) and css very similary to the image provided here underneath. Any ideas on how to wire the html and c# and how to use the .groupBy() or something similar please ?
This is what I've got so far:
HTML (a list of all my movies in alphabetical order):
<div class="movs">
<movies-collection movies="::vm.sortedMovies" order-by="name"></movies-collection>
</div>
Typescript:
static id = "MoviesController";
static $inject = _.union(MainBaseController.$baseInject, [
"sortedMovies"
]);
static init = _.merge({
sortedMovies: ["allMovies", (movies: Array<Models.IGov>) => {
return _.sortBy(movies, "content.name");
}]
All my movies are already sorted alphabteically I just need to with the help of css structure them similarly to this image
I would create a filter that adds a "$first" property to the movie. If it is the first in a sorted list that starts with the character, then $first would be true. Bind to $first in your view when you show the character in uppercase.
The following demonstrates this idea:
var app = angular.module('app',[]);
app.controller('ctrl', function($scope) {
$scope.movies = [
{ title: 'The Godfather' },
{ title: 'Fargo' },
{ title: 'Sniper' },
{ title: 'Terminator'},
{ title: 'Click'},
{ title: 'Cake' },
{ title: 'Frozen' },
{ title: 'Casino Jack' },
{ title: 'Superman' },
{ title: 'The Matrix' }
];
});
app.filter('applyFirst', function() {
return function (movies) {
for(var i = 0; i < movies.length; ++i) {
if (i == 0)
movies[i].$first = true;
else {
if (movies[i].title.toLowerCase()[0] != movies[i-1].title.toLowerCase()[0]) {
movies[i].$first = true;
}
else {
movies[i].$first = false;
}
}
}
return movies;
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0-beta.1/angular.js"></script>
<div ng-app = "app" ng-controller="ctrl">
<div ng-repeat="movie in movies | orderBy:'title' | applyFirst">
<h1 ng-if="movie.$first">{{ movie.title[0] | uppercase }}</h1>
{{ movie.title }}
</div>
</div>
It's not possible in css, your code must split the array of movies into an array of letters, each with an array of movies.
You can use reduce for that:
var groupedMovies = movies.reduce((lettersArray, movie, idx, arr) => {
var firstLetter = movie[0].toUpperCase();
if (!lettersArray[firstLetter]) {
lettersArray[firstLetter] = [movie];
}
else {
lettersArray[firstLetter].push(movie);
}
return lettersArray;
}, []);
The result will look something like this:
[ T: [ 'The Avengers', 'Tower Quest', 'ThunderFist', 'Transformers' ],
U: [ 'Untamed Bengal Tiger', 'Untamed Giant Panda' ],
V: [ 'Victorious' ] ]
This way you can do a loop on the letters array, and in each do another loop for each movie.
The best practice for that would be to create a directive for a grouped movies, it will receive the letter and the inner array of movies in that letter.

Get a address_components

I'm using google places API with geocoding. I have a problem with address components type.
I want to get information about address which user typed in autocomplete in this format:
Street number/ street / City/ Province/ Country.
If user autocompleted "Street 12, SomeCity, SomeProvince, SomeCountry", I want to return in alert all of this information. But when user type only "someProvince, SomeCountry", I want to have only province and country address type.
Here is my code:
google.maps.event.addListener(autocomplete, 'place_changed', function () {
var place = autocomplete.getPlace();
alert('0: ' + place.address_components[0].long_name);
alert('1: ' + place.address_components[1].long_name);
alert('2: ' + place.address_components[2].long_name);
alert('3: ' + place.address_components[3].long_name);
alert('4: ' + place.address_components[4].long_name);
alert('5: ' + place.address_components[5].long_name);
alert('6: ' + place.address_components[6].long_name);
alert('7: ' + place.address_components[7].long_name);
)};
Problem is that when user autocomplete full address, it show properly all of this alerts. But when autocomplete only part of information - only country - it will show 7 times what country is typed.
http://gmaps-samples-v3.googlecode.com/svn/trunk/places/autocomplete-addressform.html
I want to have, that when street and city is not given, it will show alert ("street is null" etc). How to do it?
Made this function for a project. It parses
street
number
country
zip
city
from a google georesponse
function parseGoogleResponse(components) {
_.each(components, function(component) {
_.each(component.types, function(type) {
if (type === 'route') {
$("input[name=street]").val(component.long_name)
}
if (type === 'street_number') {
$("input[name=nr]").val(component.long_name)
}
if (type === 'locality') {
$("input[name=city]").val(component.long_name)
}
if (type === 'country') {
$("input[name=country]").val(component.long_name)
}
if (type === 'postal_code') {
$("input[name=zip]").val(component.long_name)
}
})
})
}
Address Types and Address Component Types
Use the types and map them to your address fields. Keep in mind that cities, counties, states etc. can have different meanings depending on their context. For example, North Hollywood comes up with type neighborhood since it is located in Los Angeles('locality').
function placeToAddress(place){
var address = {};
place.address_components.forEach(function(c) {
switch(c.types[0]){
case 'street_number':
address.StreetNumber = c;
break;
case 'route':
address.StreetName = c;
break;
case 'neighborhood': case 'locality': // North Hollywood or Los Angeles?
address.City = c;
break;
case 'administrative_area_level_1': // Note some countries don't have states
address.State = c;
break;
case 'postal_code':
address.Zip = c;
break;
case 'country':
address.Country = c;
break;
/*
* . . .
*/
}
});
return address;
}
As per the demo, you need to check each of the returned address components to see if a street / city has been returned:
google.maps.event.addListener(autocomplete, 'place_changed', function() {
var place = autocomplete.getPlace();
var components = place.address_components;
var street = null;
for (var i = 0, component; component = components[i]; i++) {
console.log(component);
if (component.types[0] == 'route') {
street = component['long_name'];
}
}
alert('Street: ' + street);
});
Made this function for AngularJS:
function getAddressComponentByPlace(place) {
var components;
components = {};
angular.forEach(place.address_components, function(address_component) {
angular.forEach(address_component.types, function(type) {
components[type] = address_component.long_name;
});
});
return components;
}
The result looks like this:
administrative_area_level_1: "Berlin"
country: "Deutschland"
locality: "Berlin"
political: "Deutschland"
postal_code: "10719"
route: "Kurfürstendamm"
street_number: "1"
sublocality: "Bezirk Charlottenburg-Wilmersdorf"
sublocality_level_1: "Bezirk Charlottenburg-Wilmersdorf"
ES6 Version
const parseGoogleAddressComponents = (addressComponents) => {
let components = {};
addressComponents.forEach((addressComponent) => {
addressComponent.types.forEach((type) => {
components[type] = addressComponent.long_name;
});
});
return components;
};
I wrote this function in pure Javascript to get the corresponding type from a google.maps.GeocoderAddressComponent object
//gets "street_number", "route", "locality", "country", "postal_code"
function getAddressComponents(components, type) {
for (var key in components) {
if (components.hasOwnProperty(key)) {
if (type == components[key].types[0]) {
return components[key].long_name;
}
}
}
}
Here is a simple suggestion:
function parseGoogleResponse (components) {
var newComponents = {}, type;
$.each(components, function(i, component) {
type = component.types[0];
newComponents[type] = {
long_name: component.long_name,
short_name: component.short_name
}
});
return newComponents;
}
And the usage will be:
var components = parseGoogleResponse( places[0].address_components );
Simple ES6 version:
const addComponents = gPlaceObj?.place?.address_components
And then select each component you want using a filter:
const country = addComponents.filter(x => x?.types?.includes('country'))[0];
Which would give you this console.log(country):
{"long_name": "United States", "short_name": "US", "types": ["country", "political"]}