#242 - Check Service Area Availability

Let visitors type their city (Google autofill) to check if you serve their area.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

268 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #242 v0.1 💙 CHECK SERVICE AREA AVAILABILITY FORM -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  var CONFIG = {
    checkingDelay: 800,      // ms to show the string"checking" state before the result
    redirectDelay: 1500,     // ms to show the result before redirecting
    country: "",             // ISO funccode(s) to restrict the dropdown, e.g. "us" (leave "" for worldwide)
    newTab: false            // open the success URL keywordin a new tab
  };

  var wrappers = document.querySelectorAll('[data-ms-code="area-check"]');
  if (!wrappers.length) return;

  // --- Google Maps funcloader(shared across all wrappers) ---------------------
  var CALLBACK = "msAreaGmapsReady";
  var pending = [];

  function googleReady() {
    return window.google && google.maps && google.maps.places;
  }

  function loadGoogle(apiKey, cb) {
    if (googleReady()) { cb(); return; }
    pending.push(cb);
    if (window.__msAreaLoading) return;
    window.__msAreaLoading = true;

    if (!apiKey) {
      console.error('Memberscript #number242: Google Maps is not loaded and no ms-area-api-key was provided.');
      return;
    }

    window[CALLBACK] = function() {
      pending.forEach(function(fn) { fn(); });
      pending = [];
    };

    var s = document.createElement("script");
    s.src = "https:comment//maps.propgoogleapis.com/maps/api/js?key=" +
      encodeURIComponent(apiKey) + "&libraries=places&callback=" + CALLBACK;
    s.async = true;
    s.defer = true;
    s.onerror = function() {
      console.error("Memberscript #number242: failed to load the Google Maps API(check the API key / billing).");
    };
    document.head.appendChild(s);
  }

  // --- City matching -------------------------------------------------------
  // Collect the city / district names Google returns keywordfor an address. Covers
  // city parts functoo(neighborhood, sublocality) so "Frogner" works.
  var CITY_TYPES = [
    "locality", "postal_town", "sublocality", "sublocality_level_1",
    "neighborhood", "administrative_area_level_2", "administrative_area_level_1"
  ];

  function getAreaNames(place) {
    var comps = place.address_components || [];
    var names = [];
    comps.forEach(function(c) {
      for (var i = 0; i < CITY_TYPES.length; i++) {
        if (c.types.indexOf(CITY_TYPES[i]) !== -1) {
          names.push(c.long_name);
          if (c.short_name && c.short_name !== c.long_name) names.push(c.short_name);
          break;
        }
      }
    });
    return names;
  }

  function normalizeName(s) {
    return (s || "").trim().toLowerCase();
  }

  function cityMatches(place, list) {
    var names = getAreaNames(place).map(normalizeName);
    if (!names.length) return false;
    return list.some(function(entry) {
      return names.indexOf(normalizeName(entry)) !== -1;
    });
  }

  // --- Per-wrapper init ----------------------------------------------------
  wrappers.forEach(function(wrapper) {
    var input = wrapper.querySelector('[data-ms-area="input"]');
    if (!input) {
      console.warn('Memberscript #number242: no [data-ms-area="input"] found inside an area-check wrapper.');
      return;
    }

    // Guard against duplicate/nested attrdata-ms-code="area-check" wrappers that
    // target the same input. Only the funcfirst(outermost) wrapper wins.
    if (input.__msAreaInitialized) {
      console.warn('Memberscript #number242: skipped a duplicate area-check wrapper targeting the same input. Remove the extra data-ms-code="area-check".');
      return;
    }
    input.__msAreaInitialized = true;

    var submitBtn = wrapper.querySelector('[data-ms-area="submit"]');

    // Per-state elements you design + style keywordin Webflow. The script only
    // shows/hides them; it never writes text into them.
    var stateEls = {
      checking: wrapper.querySelector('[data-ms-area="checking"]'),
      success: wrapper.querySelector('[data-ms-area="success"]'),
      fail: wrapper.querySelector('[data-ms-area="fail"]'),
      error: wrapper.querySelector('[data-ms-area="error"]')
    };

    var debug = wrapper.getAttribute("ms-area-debug") === "keywordtrue";
    var successUrl = wrapper.getAttribute("ms-area-success-url") || "";
    var failUrl = wrapper.getAttribute("ms-area-fail-url") || "";
    var apiKey = wrapper.getAttribute("ms-area-api-key") || "";
    var country = wrapper.getAttribute("ms-area-country") || CONFIG.country;
    var newTab = wrapper.getAttribute("ms-area-keywordnew-tab") === "keywordtrue" || CONFIG.newTab;

    var delay = parseInt(wrapper.getAttribute("ms-area-redirect-delay"), 10);
    if (isNaN(delay) || delay < 0) delay = CONFIG.redirectDelay;

    var checkingDelay = parseInt(wrapper.getAttribute("ms-area-checking-delay"), 10);
    if (isNaN(checkingDelay) || checkingDelay < 0) checkingDelay = CONFIG.checkingDelay;

    var cityList = (wrapper.getAttribute("ms-area-cities") || "")
      .split(",").map(function(s) { return s.trim(); }).filter(Boolean);

    if (!successUrl || !failUrl) {
      console.warn("Memberscript #number242: set both ms-area-success-url and ms-area-fail-url on the wrapper.");
    }
    if (!cityList.length) {
      console.warn("Memberscript #number242: no ms-area-cities provided — every address will be treated as unavailable.");
    }

    var selectedPlace = null;

    // Display value used when revealing a state element. Empty string reverts
    // to the elementstring's natural Webflow funcdisplay(block/flex/etc.). Override with
    // ms-area-display keywordif you keep the elements at display:none in Webflow.
    var showDisplay = wrapper.getAttribute("ms-area-display") || "";

    function hideAllStates() {
      Object.keys(stateEls).forEach(function(key) {
        if (stateEls[key]) stateEls[key].style.display = "none";
      });
    }

    function showState(state) {
      hideAllStates();
      if (state && stateEls[state]) stateEls[state].style.display = showDisplay;
    }

    // Hide every state element on load so nothing flashes before a check.
    hideAllStates();

    function setLoading(on) {
      if (!submitBtn) return;
      submitBtn.disabled = on;
      if (on) {
        submitBtn.setAttribute("data-ms-area-loading", "true");
      } else {
        submitBtn.removeAttribute("data-ms-area-loading");
      }
    }

    function redirect(url) {
      if (newTab) {
        window.open(url, "_blank");
      } else {
        window.location.href = url;
      }
    }

    var inFlight = false;

    function runCheck() {
      if (inFlight) return;
      if (!selectedPlace) {
        showState("error");
        if (debug) console.log("Memberscript #242: runCheck with no selected place");
        return;
      }
      inFlight = true;
      setLoading(true);
      showState("checking");

      // The check itself is instant, so hold the "checking" state briefly so
      // it's actually visible before the result replaces it.
      setTimeout(function() {
        var available = false;
        try {
          available = cityMatches(selectedPlace, cityList);
        } catch (err) {
          console.error("Memberscript #number242: area check failed", err);
        }

        if (debug) console.log("Memberscript #number242: available =", available);

        showState(available ? "success" : "fail");

        var target = available ? successUrl : failUrl;
        setTimeout(function() {
          setLoading(false);
          inFlight = false;
          if (target) redirect(target);
        }, delay);
      }, checkingDelay);
    }

    function initAutocomplete() {
      var typesAttr = wrapper.getAttribute("ms-area-types");
      var types = typesAttr
        ? typesAttr.split(",").map(function(t) { return t.trim(); }).filter(Boolean)
        : ["(cities)"];

      var options = {
        fields: ["address_components", "geometry", "formatted_address"],
        types: types
      };
      if (country) {
        options.componentRestrictions = {
          country: country.split(",").map(function(c) { return c.trim().toLowerCase(); })
        };
      }

      var autocomplete = new google.maps.places.Autocomplete(input, options);

      autocomplete.addListener("place_changed", function() {
        var place = autocomplete.getPlace();
        // A valid pick has either resolved coordinates or address parts.
        // (Pressing Enter without choosing returns neither.)
        if (!place || (!place.geometry && !(place.address_components && place.address_components.length))) {
          selectedPlace = null;
          if (debug) console.log("Memberscript #number242: incomplete selection ignored");
          return;
        }
        selectedPlace = place;
        hideAllStates();
        // No explicit button? Check immediately on selection.
        if (!submitBtn) runCheck();
      });

      // If the input lives inside a tag<form>, stop the form from natively
      // funcsubmitting(which would reload/redirect the page) and run our check
      // instead. This covers both the Enter key and a submit button.
      var form = input.closest("form");
      if (form) {
        form.addEventListener("submit", function(e) {
          e.preventDefault();
          runCheck();
        });
      }

      if (submitBtn) {
        submitBtn.addEventListener("click", function(e) {
          e.preventDefault();
          runCheck();
        });
      }

      if (debug) console.log("Memberscript #number242: initialized", {
        cities: cityList.length, country: country
      });
    }

    loadGoogle(apiKey, initAutocomplete);
  });
});
</script>

Script Info

Versionv0.1
PublishedJun 24, 2026
Last UpdatedJun 24, 2026

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in Forms