You are viewing limited content. For full access, please sign in.

Discussion

Discussion

Google Maps Address Autocomplete in Modern Forms (and why I use a proxy field)

posted on March 4 Show version history

Hello everyone,

I recently worked on integrating the Google Maps Places API for address autocomplete in Laserfiche Modern Forms, and I wanted to share a working approach and explain a specific workaround that is required to make it function correctly.

I'm sharing this in hopes it helps someone else, but I am also very open to feedback. If anyone in the community has found a way to improve this solution, or if you've successfully managed to hook Google Autocomplete directly into the native Modern Forms Address block without running into these React conflicts, please feel free to share! Better approaches and optimizations are more than welcome.
 

The Challenge with the Native Address Block Initially, the goal was to attach the Google Autocomplete dropdown directly to the "Street Address" sub-field inside the native Laserfiche Address block. However, because Modern Forms is built on React, it aggressively manages the DOM, state, and input events.

When you try to hook Google's native UI directly into the Address block's sub-field, React's internal event handling clashes with Google Maps. This results in the dropdown list either being hidden, failing to trigger when typing, or the input freezing entirely. Furthermore, the dynamic IDs in Modern Forms make it very difficult to reliably target that specific sub-field.

The Solution: The "Proxy" Search Field To achieve a smooth and fast user experience without fighting the React engine, the best solution is to use a separate Single Line text field as a dedicated search bar.

Here is how the flow works:

  1. Create a Single Line field (e.g., Address_Search) and place it right above your native Address block.

  2. Attach the Google Autocomplete script strictly to this Single Line field. Since it's a simple text input, React doesn't interfere with Google's UI, and the dropdown appears instantly as the user types.

  3. Map the Data: When the user selects an address from the Google dropdown, the JavaScript captures the Google Place object, parses the components (Street, City, State, Zip, Country), and maps them to Laserfiche's format.

  4. Auto-Fill the Native Block: Finally, the script uses LFForm.setFieldValues to silently push the formatted data into the actual native Address block (e.g., Address_Main) in one go.

This approach completely bypasses the React DOM conflicts, keeps the code clean, and provides a seamless, real-time autocomplete experience for the end user.

I hope this explanation saves time for anyone trying to implement a similar solution in Modern Forms!

PS: In the example image you will see I put a Radio Button questions The address is correct? the idea is prevent users type any character after put the address, but that is optional to you.

Here the JS:

 

(function () {

  // =========================
  // CONFIGURATION
  // =========================
  // [NOTE: This is where users should input their own credentials and Laserfiche variable names]
  const GOOGLE_KEY      = "YOUR_GOOGLE_API_KEY_HERE"; // Google Places API Key
  const SEARCH_FIELD    = { variableName: "Address_Search" }; // The Single Line "Proxy" field where the user types
  const ADDRESS_FIELD   = { variableName: "Address_Main" }; // The native Laserfiche Address block that will be auto-filled
  const MIN_CHARS       = 2; // Minimum characters required before calling the API
  const DEBOUNCE_MS     = 300; // Wait time (in milliseconds) after typing stops to prevent API spam
  const MAX_SUGGESTIONS = 8; // Maximum number of suggested addresses to display

  // =========================
  // STATE
  // =========================
  // [NOTE: Internal variables to manage cache memory and prevent React rendering errors]
  const placeIdByDescription = new Map(); // Stores searched addresses in memory to keep track of the 'Place ID'
  let autocompleteService;
  let placesService;
  let debounceTimer     = null; // Controls the typing debounce timer
  let requestSeq        = 0; // Prevents slow API responses from overwriting newer ones
  let isSelecting       = false; // Flag to determine if the user is selecting an address or just typing
  let lastSuggestionKey = ""; // Prevents reloading the same address list if it hasn't changed

  // =========================
  // GOOGLE LOADER
  // =========================
  // [NOTE: Safely injects the Google Maps script into the form. 
  // Checks if it already exists to avoid loading it twice and causing console errors]
  function loadGooglePlaces(key) {
    return new Promise((resolve, reject) => {
      if (window.google?.maps?.places?.AutocompleteService) return resolve();

      let s = document.querySelector("script[data-lf-google-places]");
      if (s) {
        s.addEventListener("load", resolve);
        s.addEventListener("error", () => reject(new Error("Google script failed to load")));
        return;
      }

      s = document.createElement("script");
      s.setAttribute("data-lf-google-places", "1");
      s.src =
        "https://maps.googleapis.com/maps/api/js" +
        "?key=" + encodeURIComponent(key) +
        "&libraries=places&v=weekly";
      s.async = true;
      s.defer = true;
      s.onload  = () =>
        window.google?.maps?.places?.AutocompleteService
          ? resolve()
          : reject(new Error("Places library unavailable after load"));
      s.onerror = () => reject(new Error("Failed to load Google Maps script"));
      document.head.appendChild(s);
    });
  }

  // =========================
  // ADDRESS PARSING
  // =========================
  // [NOTE: Converts Google's complex data format into a clean object 
  // that exactly matches the Laserfiche Address block sub-fields]
  function extract(components, type, short = true) {
    const c = (components || []).find(x => x.types?.includes(type));
    return c ? (short ? c.short_name : c.long_name) || "" : "";
  }

  function buildLFAddress(place) {
    const comps  = place.address_components || [];
    const postal = extract(comps, "postal_code");
    const suffix = extract(comps, "postal_code_suffix");
    return {
      address1: [extract(comps, "street_number"), extract(comps, "route")]
                  .filter(Boolean).join(" ").trim(),
      address2: "",
      city:     extract(comps, "locality") || extract(comps, "postal_town"),
      province: extract(comps, "administrative_area_level_1") ||
                extract(comps, "administrative_area_level_2"),
      zipcode:  suffix ? `${postal}-${suffix}` : postal,
      country:  extract(comps, "country"),
    };
  }

  // =========================
  // GOOGLE API CALLS
  // =========================
  // [NOTE: Functions that communicate directly with Google servers 
  // to fetch text predictions and exact location details]
  function getPredictions(input) {
    return new Promise((resolve) => {
      autocompleteService.getPlacePredictions(
        { input, types: ["address"] },
        (preds, status) => {
          if (status !== google.maps.places.PlacesServiceStatus.OK || !preds) return resolve([]);
          resolve(preds);
        }
      );
    });
  }

  function getDetails(placeId) {
    return new Promise((resolve, reject) => {
      placesService.getDetails(
        { placeId, fields: ["address_components"] },
        (place, status) => {
          if (status !== google.maps.places.PlacesServiceStatus.OK || !place)
            return reject(new Error(`PlacesService error: ${status}`));
          resolve(place);
        }
      );
    });
  }

  // =========================
  // SUGGESTION MANAGEMENT
  // =========================
  // [NOTE: Takes the addresses returned by Google and injects them as options 
  // into the native dropdown list of the "Address_Search" field in Laserfiche]
  async function setSuggestions(list) {
    const key = list.join("\u0001");
    if (key === lastSuggestionKey) return;
    lastSuggestionKey = key;
    await LFForm.changeFieldSettings(SEARCH_FIELD, { autoCompleteValues: list });
  }

  async function clearSuggestions() {
    if (lastSuggestionKey === "") return;
    lastSuggestionKey = "";
    await LFForm.changeFieldSettings(SEARCH_FIELD, { autoCompleteValues: [] });
  }

  async function refreshSuggestions(input) {
    const trimmed = (input || "").trim();
    if (trimmed.length < MIN_CHARS) { await clearSuggestions(); return; }

    const mySeq = ++requestSeq;
    try {
      const preds = await getPredictions(trimmed);
      if (mySeq !== requestSeq) return;
      const list = preds.slice(0, MAX_SUGGESTIONS).map(p => {
        placeIdByDescription.set(p.description, p.place_id);
        return p.description;
      });
      await setSuggestions(list);
    } catch (e) {
      if (mySeq !== requestSeq) return;
      await clearSuggestions();
    }
  }

  function scheduleRefresh(value) {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => refreshSuggestions(value), DEBOUNCE_MS);
  }

  // =========================
  // SELECTION HANDLER
  // =========================
  // [NOTE: Executes when the user clicks an address from the list. 
  // It clears the native address block (to prevent React cache errors) 
  // and then injects the final formatted address]
  async function applySelection(value) {
    const text    = (value || "").trim();
    const empty   = { address1: "", address2: "", city: "", province: "", zipcode: "", country: "" };

    if (!text) { await LFForm.setFieldValues(ADDRESS_FIELD, empty); return; }

    const placeId = placeIdByDescription.get(text);
    if (!placeId) return;

    try {
      const place = await getDetails(placeId);
      const addr  = buildLFAddress(place);
      await LFForm.setFieldValues(ADDRESS_FIELD, empty);
      await LFForm.setFieldValues(ADDRESS_FIELD, addr);
    } catch (e) {
      console.error("[LF Autocomplete] Failed to get place details:", e);
    }
  }

  // =========================
  // INIT
  // =========================
  // [NOTE: Script entry point. Hooks into Laserfiche's native 'onFieldChange' event 
  // to listen to the user's typing and trigger the flow]
  async function init() {
    await loadGooglePlaces(GOOGLE_KEY);

    autocompleteService = new google.maps.places.AutocompleteService();
    const dummy = document.createElement("div");
    document.body.appendChild(dummy);
    placesService = new google.maps.places.PlacesService(dummy);

    LFForm.onFieldChange(function () {
      if (isSelecting) return;
      const current = (LFForm.getFieldValues(SEARCH_FIELD) || "").trim();

      // If the typed text exactly matches an address in our cache, 
      // it means the user selected it from the dropdown.
      if (placeIdByDescription.has(current)) {
        isSelecting = true;
        applySelection(current).finally(() => { isSelecting = false; });
        return;
      }

      // If not, it means they are still typing, so we fetch new suggestions.
      scheduleRefresh(current);
    }, SEARCH_FIELD);
  }

  init().catch(err => console.error("[LF Autocomplete] Init error:", err));

})();

How this code works (A breakdown for developers):

To make this code as plug-and-play as possible, it is divided into distinct logical blocks:

  1. CONFIGURATION: This is the primary section you need to edit. Paste your Google API Key and ensure the variableName properties perfectly match the names of your Laserfiche fields (the single-line proxy search field and the main address block). You can also tweak how many characters trigger a search (MIN_CHARS) and the typing delay (DEBOUNCE_MS) to optimize API usage.

  2. STATE: Because Modern Forms relies on React, the DOM can be unpredictable. We use a Map (placeIdByDescription) as a temporary memory cache to link the text you see in the dropdown with its unique Google place_id. This prevents data loss if React redraws the field in the background.

  3. GOOGLE LOADER: Instead of pasting an external script tag into the form's custom HTML (which can cause issues in Laserfiche), this function dynamically and safely injects the Google Maps API into the page and ensures it only loads once.

  4. ADDRESS PARSING (buildLFAddress): Google returns addresses in a complex array format. This function acts as a translator. It extracts the street, city, zip code, etc., and packages them into a clean JavaScript object that perfectly mirrors the internal structure of a Laserfiche Address block.

  5. SUGGESTION MANAGEMENT: As the user types, the scheduleRefresh function acts as a "debouncer". It waits until the user pauses typing (for a set duration, like 300ms) before asking Google for suggestions. Once Google responds, setSuggestions uses the native Laserfiche API to populate the dropdown list of our proxy search field.

  6. SELECTION HANDLER (applySelection): This is where the mapping happens. When the user clicks an address, this function fetches the full details from Google. To prevent React caching bugs where old data refuses to update, it first pushes a completely blank address into the form to wipe it clean, and then instantly pushes the newly formatted address data, populating the entire Address block simultaneously.

GAutocomplete-ModernDesign.gif
Address-Form.png
Address-Form-Field.png
Address-Search-Field.png
Address-Form.png (16.94 KB)
8 0
replied on April 14

I could be wrong, but I believe the Places API has been deprecated for new customers in favor of Places (New) which uses different syntax. I tried to test your code out on a simple form with 2 fields (a search field and an address block) but it does not work, and was given the following warnings which explain why:

 

As of March 1st, 2025, google.maps.places.AutocompleteService is not available to new customers. Please use google.maps.places.AutocompleteSuggestion instead. At this time, google.maps.places.AutocompleteService is not scheduled to be discontinued, but google.maps.places.AutocompleteSuggestion is recommended over google.maps.places.AutocompleteService. While google.maps.places.AutocompleteService will continue to receive bug fixes for any major regressions, existing bugs in google.maps.places.AutocompleteService will not be addressed. At least 12 months notice will be given before support is discontinued. Please see https://developers.google.com/maps/legacy for additional details and https://developers.google.com/maps/documentation/javascript/places-migration-overview for the migration guide.

As of March 1st, 2025, google.maps.places.PlacesService is not available to new customers. Please use google.maps.places.Place instead. At this time, google.maps.places.PlacesService is not scheduled to be discontinued, but google.maps.places.Place is recommended over google.maps.places.PlacesService. While google.maps.places.PlacesService will continue to receive bug fixes for any major regressions, existing bugs in google.maps.places.PlacesService will not be addressed. At least 12 months notice will be given before support is discontinued. Please see https://developers.google.com/maps/legacy for additional details and https://developers.google.com/maps/documentation/javascript/places-migration-overview for the migration guide.

 

0 0
replied on March 5

This is fantastic!  Thank you for sharing, I will definitely be using this.

1 0
replied on March 5

Hi @████████, I'm glad you found it useful. It's a great compliment to me. Thank you. 

0 0
replied on March 4

Luis this is awesome! I created an integration with Google Maps for the classic designer back when Forms was first released but haven't needed to recreate it in the modern designer yet. As one who has been through this pain before, thank you for taking the time to do it. Would you be okay if I created an instructional video about the solution you made?

3 0
replied on March 4

Hi @████████I truly appreciate your message; it means a lot and is a great compliment. Please feel free to use the code and this solution in any way you find useful. The goal is for it to help as many people as possible. It would be fantastic if you decided to create a video about it; I’m sure many of us would really appreciate it.

2 0
replied on April 10

Just a heads up I stole redid your code using the google places API and have it interacting directly with the form via the LFForm API. I also added address validation using the API as well. Its fairly code heavy, but setup to be modular for any form environment.

I'm presenting it at my advanced JS class at empower this year and will release the code afterwards!

1 0
replied on April 11

@████████ 

That’s fantastic to hear! I’m truly glad that my small contribution helped inspire you to take this to the next level. I’m confident it will become something that many people will genuinely enjoy.

I was really looking forward to attending this year, as I believe it has the potential to be one of the best Laserfiche Empower conferences yet. However, it seems unlikely that my company will be able to cover my attendance this time. If I’m ultimately unable to make it, I’ll need to explore alternative ways to access the event content.

0 0
replied on April 13

My course will be recorded in our Aspire training platform after Empower so no worries! The git repository I mentioned in our other thread should hopefully be intuitive enough to use that you won't need my presentation.

1 0
replied on April 13

Hi Zac, just wanted to see if you will be separating the code more than what you had last year? Last year's code was great, but because it was all intertwined it was harder to separate out. I'm really excited for your class again this year!

0 0
replied on April 13

Ya (assuming you all pass my test), I'm going to publish my entire repository.

  1. typesafe LFForm object
  2. Helper functions
  3. Core library (modals, table helpers, etc.)
  4. Examples (this + last years forms and integration code)

 

2 0
replied on March 4

The code looks good! More robust than the one I wrote, but still reusable/copypastable. I would recommend adding more comments to the configuration/state variables to explain what they do. State likely doesn't need to be touched, but I had to do a lot of scrolling up and down to see what the variables were being used for inside the IIFE. Its more important for the configuration side since that is what other people would need to change to get it to work on their form.

I agree that the proxy search field is the only way to solve this specific problem since you are limited in customizations of the address field. I had played with building my own address field where the proxy search field was the address line 1 field, but it was very painful to handle differentiating setting the value in code and the user attempting to search.

I also like the inclusion of the address correction radios so you can use field rules to enable/disable the address field!

 

It would be worth submitting this as a solution template since its applicable to cloud and self-hosted! 
https://portal.laserfiche.com/w6303/forms/SolutionTemplateSubmission

2 0
replied on March 4 Show version history

Hi @████████, thank you so much for your message! I truly appreciate you taking the time to write this and share your thoughts. 

Honestly, I didn't write all the code myself. I relied heavily on ChatGPT at the beginning, then refined the code with Claude AI, and then consulted with a friend who is a full-stack programmer and he gave me the green light to use it because he thought it was very well done, for this case.

You made a really great point about the comments. I completely agree that making the configuration and state variables easier to understand is crucial for anyone trying to adapt this to their own forms. Taking your advice, I’ve just updated my post with a fully commented version of the code, breaking down exactly what needs to be changed in the Config section.

It’s incredibly validating to hear that you came to the same conclusion regarding the proxy search field. Trying to force the autocomplete directly into the native Address Line 1 field while fighting the React DOM was definitely more painful than it was worth! I'm also glad you liked the radio button implementation for the address correction; it really does help keep the form rules clean and the UI intuitive.

Also, thank you for the recommendation to submit this as a solution template! I really appreciate your consideration and think that's a fantastic idea. Since this approach works seamlessly for both Cloud and Self-Hosted environments, I will definitely look into putting that together and submitting it.

Thanks again for the great collaboration, this is exactly what makes this community so great!"

2 0
replied on March 4

I’m interested in knowing your process. I have my own tooling for prompting AI with the very specific nature it needs to be coded to be compatible with the LFForm object. Did you just give it a link to the docs?

1 0
replied on March 5

Hi @████████, in my humble experience working with AI, I don't usually stick to specific formulas because they rarely work (in my case). Each project, even if it seems similar, is unique, and I treat them as such. This way, I prevent the AI ​​from getting carried away too quickly, especially when a project might require multiple prompt attempts and sometimes the use of different AI tools to achieve the best possible result.

I try to start with the simplest approach, without overcomplicating things when explaining to the AI ​​(ChatGPT) what I need. I give it a general context of the problem and use that first prompt, giving the AI ​​complete freedom to analyze what I'm asking for to see how well it understands my requirements. Sometimes, by doing this, I discover that I occasionally omit details in my prompt, and the result of that first prompt helps me correct it. Then, I open another new chat with a more elaborate prompt (as close as possible to Markdown) where I define the role, the problem, details of the environment and tools I have available, the conditions, and what I hope to achieve. And I always, always provide the URLs of the official documentation for what I need to consult, because the AI's knowledge databases are generally not up-to-date, and this pushes and forces the AI ​​to consult the most recent information and work from there on a solution. (By doing this alone, I almost always solve 90-99% of the problem I'm working on in the first 2 to 3 prompts.)

Even after the problem is solved, whether it works or not, what I usually do to refine the solution is take all the resulting code and analyze it with Claude Code. I don't just give it the code; I also provide the context and show it the code as a possible solution that I need it to review and analyze. I also give it the URLs of the official documentation so it can provide the best answer. At this stage, I always use the best AI engines available in the paid plan I'm using (whether it's ChatGPT, Google Gemini, or Claude), but in the final phase, since it's code I need to adjust, Claude is the best option so far.

Something else I do is go to the Laserfiche documentation page and use the chatbot to see if it can find me or give me any suggestions. Sometimes it helps me understand better. I'm not sure if this meets your expectations, but honestly, I'm not holding anything back. I try to be as logical as possible when looking for a solution (it's not always easy). I'm always willing to collaborate and help as much as I can. So, if there's anything I can help you with, just let me know.

0 0
replied on March 6

No this is perfect! I'm trying to standardize/streamline work like this using docs/and supplemental context to get better responses from AI.

0 0
You are not allowed to follow up in this post.

Sign in to reply to this post.