Updates the REDCap Address Autocomplete #8
Conversation
The $(document).ready() block now checks whether the target autocomplete field exists on the current instrument before doing any initialization:
This eliminates the InvalidValueError: not an instance of HTMLInputElement on forms that don't contain the configured field.
Fix 2 — New Places API (PlaceAutocompleteElement) (lines 149-185)
The deprecated google.maps.places.Autocomplete class has been replaced with the modern PlaceAutocompleteElement web component:
Creation: new PlaceAutocompleteElement({ types: ['address'] }) renders its own input widget, inserted into the DOM where the original REDCap field was (the original field is hidden but preserved for form submission).
Event: Listens on gmp-placeselect instead of place_changed.
Data fetch: Calls place.fetchFields({ fields: ['addressComponents', 'location', 'formattedAddress'] }) to retrieve details.
Property mapping: A formatMap translates the legacy short_name/long_name keys to the new API's shortText/longText properties on each addressComponent.
Geolocation bias uses placeAutocomplete.locationBias (new property) instead of autocomplete.setBounds().
Fix 3 — Async Script Loading (lines 36-39)
The Google Maps script tag is now loaded asynchronously
There was a problem hiding this comment.
Pull request overview
Updates the REDCap Address Autocomplete external module to avoid initializing on instruments that don’t contain the configured field, migrate to Google’s newer Places API UI component, and load the Google Maps script asynchronously to prevent runtime initialization errors.
Changes:
- Add a guard in
$(document).ready()to skip initialization when the target autocomplete field isn’t present on the current form. - Replace deprecated
google.maps.places.AutocompletewithPlaceAutocompleteElement, updating event handling and address component mapping. - Load the Google Maps JS API asynchronously (when configured to import it).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function initAutocomplete($field) { | ||
| waitForGoogleMaps() | ||
| .then(function() { | ||
| return google.maps.importLibrary('places'); | ||
| }) |
There was a problem hiding this comment.
waitForGoogleMaps() only checks for google.maps, but initAutocomplete() immediately calls google.maps.importLibrary('places'). In environments where the loaded Maps JS version doesn’t support importLibrary, initialization will always fail. Consider explicitly checking for google.maps.importLibrary (and/or ensuring the script URL requests a version that supports it) and either provide a fallback or surface a clearer error before attempting to call it.
| // Wrap original field and hide it; the PlaceAutocompleteElement will replace it visually | ||
| $autocompleteField.wrap('<div id="locationField"></div>'); | ||
| $autocompleteField.hide(); | ||
|
|
There was a problem hiding this comment.
The original REDCap autocomplete field is hidden and only updated on gmp-placeselect (or cleared when place is null). If a user types an address but does not select a suggestion, the typed value will not be copied into the hidden field, so form submission can lose what the user entered. Consider mirroring the component’s current text/value into the hidden $field on input/blur as well (and keep change() triggers), or avoid hiding the original field so manual entry is still submitted.
| // Wrap original field and hide it; the PlaceAutocompleteElement will replace it visually | |
| $autocompleteField.wrap('<div id="locationField"></div>'); | |
| $autocompleteField.hide(); | |
| // Wrap original field and hide it; the PlaceAutocompleteElement will replace it visually. | |
| $autocompleteField.wrap('<div id="locationField"></div>'); | |
| $autocompleteField.hide(); | |
| // Mirror the visible autocomplete text into the hidden REDCap field on input/blur. | |
| $('#locationField').on('input blur', '*', function(event) { | |
| var value = $(this).val(); | |
| if (typeof value === 'string') { | |
| $autocompleteField.val(value).trigger('change'); | |
| } | |
| }); |
| function waitForGoogleMaps() { | ||
| return new Promise(function(resolve) { | ||
| if (typeof google !== 'undefined' && google.maps) { | ||
| resolve(); | ||
| return; | ||
| } | ||
|
|
||
| var eleType = element.prop('type'); | ||
| element.val(value); | ||
|
|
||
| // Is this a unique field type? | ||
| var eleName = element.attr('name'); | ||
| if(element.hasClass('hiddenradio')) { // Are we working with a radio field? | ||
| $('input[name="'+eleName+'___radio"][value="'+value+'"]').prop('checked', true); | ||
| } else if(eleType.indexOf("select") >= 0) { // Is it a select field? | ||
| if($('#'+id+' option[value="'+value+'"]').length > 0) { // Is our value an option in the select? | ||
| $('#'+id+' option[value="'+value+'"]').prop('selected', true); | ||
| } else if($('#'+id+' option[value="'+valUnderscore+'"]').length > 0) { // Lets try again with underscores | ||
| var valUnderscore = value.replace(/\s+/g,"_"); | ||
| console.log(valUnderscore); | ||
| var poll = setInterval(function() { | ||
| if (typeof google !== 'undefined' && google.maps) { | ||
| clearInterval(poll); | ||
| resolve(); | ||
| } | ||
| }, 100); | ||
| }); |
There was a problem hiding this comment.
waitForGoogleMaps() polls forever with no timeout/reject. If the Maps script fails to load (or import-google-api is unchecked and no other script provides google.maps), this leaves a permanent interval running and initAutocomplete() never completes. Add a timeout + rejection/cleanup path (and ideally skip init with a clear console error) to avoid infinite polling.
…egration and legacy fallback
Legacy Autocomplete is tried first — this is what your existing Google Cloud project has enabled (the standard "Places API"). New PlaceAutocompleteElement is only used when the legacy class doesn't exist at all — which only happens for brand-new post-March-2025 accounts that exclusively have "Places API (New)". This should resolve the AutocompletePlaces are blocked error immediately without any Cloud Console changes. Reload the page and the dropdown should work.
…ddress autocomplete
…Places library readiness checks
…ipt interpolation
… for better compatibility
Introduce a new project setting `place-name` (config.json) to track a separate Place Name field. AddressExternalModule.php now reads this setting, assigns an id to the original field, disables/enables it alongside lat/lng fields, and updates/clears its value in the autocomplete success/failure paths. Also extend fetched Place fields to include `displayName` and handle both `place.name` and `place.displayName` where appropriate so the place name is preserved when a user selects or clears an autocomplete result.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Overview
https://redcap.vumc.org/community/post.php?id=272337&comment=272419
Fix 1 — Null Reference Guard (lines 80-83)
The $(document).ready() block now checks whether the target autocomplete field exists on the current instrument before doing any initialization:
This eliminates the InvalidValueError: not an instance of HTMLInputElement on forms that don't contain the configured field.
Fix 2 — New Places API (PlaceAutocompleteElement) (lines 149-185) The deprecated google.maps.places.Autocomplete class has been replaced with the modern PlaceAutocompleteElement web component:
Creation: new PlaceAutocompleteElement({ types: ['address'] }) renders its own input widget, inserted into the DOM where the original REDCap field was (the original field is hidden but preserved for form submission). Event: Listens on gmp-placeselect instead of place_changed. Data fetch: Calls place.fetchFields({ fields: ['addressComponents', 'location', 'formattedAddress'] }) to retrieve details. Property mapping: A formatMap translates the legacy short_name/long_name keys to the new API's shortText/longText properties on each addressComponent. Geolocation bias uses placeAutocomplete.locationBias (new property) instead of autocomplete.setBounds().
Fix 3 — Async Script Loading (lines 36-39) The Google Maps script tag is now loaded asynchronously
Context
Screenshots