Skip to content

Commit

Permalink
Merge pull request #32 from Woosmap/dev/localities-api
Browse files Browse the repository at this point in the history
Feat : Localities api autocomplete endpoint samples
  • Loading branch information
gaelsimon authored Feb 28, 2024
2 parents 920a481 + 988b38b commit 59aaf9c
Show file tree
Hide file tree
Showing 10 changed files with 529 additions and 6 deletions.
21 changes: 15 additions & 6 deletions e2e/samples/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { test } from "@playwright/test";
import { waitForWoosmapToLoad, failOnPageError } from "../utils";
import {
waitForWoosmapToLoad,
waitForAutocompleteFetch,
failOnPageError,
} from "../utils";
import fs from "fs";

export const BROKEN_APP_SAMPLES = [
"store-locator-widget-baidu", // too long to load
];
export const AUTOCOMPLETE_WITHOUT_MAP_SAMPLES = [
"localities-api-autocomplete",
"localities-api-autocomplete-advanced",
];

const samples = fs
.readdirSync("samples", { withFileTypes: true })
Expand All @@ -24,12 +32,13 @@ test.describe.parallel("sample applications", () => {
waitUntil: "networkidle",
});

if (sample === "programmatic-load-button") {
await page.locator("button").click();
if (AUTOCOMPLETE_WITHOUT_MAP_SAMPLES.includes(sample)) {
// wait for #suggestions-list li elements to be visible
await waitForAutocompleteFetch(page);
} else {
// wait for woosmap.map to be loaded
await waitForWoosmapToLoad(page);
}

// wait for woosmap.map to be loaded
await waitForWoosmapToLoad(page);
});
});
});
Expand Down
7 changes: 7 additions & 0 deletions e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export async function waitForWoosmapToLoad(page: Page): Promise<void> {
await page.waitForTimeout(100);
}

export async function waitForAutocompleteFetch(page: Page): Promise<void> {
await page.fill("#autocomplete-input", "Paris");
await page.waitForSelector(`//ul[@id='suggestions-list']/li`, {
state: "visible",
});
}

export const failOnPageError = (page: Page): void => {
page.on("pageerror", (e) => {
console.error(e.message);
Expand Down
22 changes: 22 additions & 0 deletions samples/localities-api-autocomplete-advanced/index.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends '../../src/_includes/layout.njk' %}
{% block html %}
<!-- [START woosmap_{{ tag }}_div] -->
<div id="app">
<div id="autocomplete-container">
{% svgIcon 'search.svg' %}
<input
type="text"
id="autocomplete-input"
placeholder="Search a locality or a postal code..."
autocomplete="off"
/>
<button aria-label="Clear" class="clear-searchButton" type="button">
{% svgIcon 'clear.svg' %}
</button>
<ul id="suggestions-list"></ul>
</div>
<pre id="response-container"></pre>
</div>
<!-- [END woosmap_{{ tag }}_div] -->
{% endblock %}
{% block api %}{% endblock %}
208 changes: 208 additions & 0 deletions samples/localities-api-autocomplete-advanced/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// [START woosmap_localities_api_autocomplete_advanced]
const componentsRestriction = [];
const woosmap_key = "YOUR_API_KEY";
let debouncedAutocomplete: (
...args: any[]
) => Promise<woosmap.map.localities.LocalitiesAutocompleteResponse>;

const inputElement = document.getElementById(
"autocomplete-input",
) as HTMLInputElement;
const suggestionsList = document.getElementById(
"suggestions-list",
) as HTMLUListElement;
const clearSearchBtn = document.getElementsByClassName(
"clear-searchButton",
)[0] as HTMLButtonElement;
const responseElement = document.getElementById(
"response-container",
) as HTMLElement;

function init(): void {
if (inputElement && suggestionsList) {
inputElement.addEventListener("input", handleAutocomplete);
inputElement.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
const firstLi = suggestionsList.querySelector("li");
if (firstLi) {
firstLi.click();
}
}
});
}
clearSearchBtn.addEventListener("click", () => {
inputElement.value = "";
suggestionsList.style.display = "none";
clearSearchBtn.style.display = "none";
responseElement.innerHTML = "";
inputElement.focus();
});
debouncedAutocomplete = debouncePromise(autocompleteAddress, 0);
}

function handleAutocomplete(): void {
if (inputElement && suggestionsList) {
const input = inputElement.value;
input.replace('"', '\\"').replace(/^\s+|\s+$/g, "");
const components: string[] = componentsRestriction.map(
({ id }) => `country:${id}`,
);
const componentsArgs: string = components.join("|");
if (input !== "") {
debouncedAutocomplete(input, componentsArgs, woosmap_key)
.then(({ localities }) => displaySuggestions(localities))
.catch((error) =>
console.error("Error autocomplete localities:", error),
);
}
}
}
function displaySuggestions(
localitiesPredictions: woosmap.map.localities.LocalitiesPredictions[],
) {
if (inputElement && suggestionsList) {
suggestionsList.innerHTML = "";
if (localitiesPredictions.length > 0) {
localitiesPredictions.forEach((locality) => {
const li = document.createElement("li");
li.innerHTML = formatPredictionList(locality) ?? "";
li.addEventListener("click", () => {
inputElement.value = locality.description ?? "";
suggestionsList.style.display = "none";
displayLocalitiesResponse(locality);
});
suggestionsList.appendChild(li);
});
suggestionsList.style.display = "block";
clearSearchBtn.style.display = "block";
} else {
suggestionsList.style.display = "none";
}
}
}
function formatPredictionList(locality): string {
const prediction = locality;
const predictionClass = "no-viewpoint";
const matched_substrings = prediction.matched_substrings;
let formatted_name = "";
if (
prediction.matched_substrings &&
prediction.matched_substrings.description
) {
formatted_name = bold_matched_substring(
prediction["description"],
matched_substrings.description,
);
} else {
formatted_name = prediction["description"];
}

let html = "";
html += `<div class="prediction ${predictionClass}">${formatted_name}</div>`;

return html;
}
function displayLocalitiesResponse(
selectedLocality: woosmap.map.localities.LocalitiesPredictions,
) {
if (responseElement) {
responseElement.innerHTML = `<code>${JSON.stringify(selectedLocality, null, 2)}</code>`;
}
}
function bold_matched_substring(string: string, matched_substrings: string[]) {
matched_substrings = matched_substrings.reverse();
for (const substring of matched_substrings) {
const char = string.substring(
substring["offset"],
substring["offset"] + substring["length"],
);
string = `${string.substring(
0,
substring["offset"],
)}<span class='bold'>${char}</span>${string.substring(
substring["offset"] + substring["length"],
)}`;
}
return string;
}
function autocompleteAddress(
input: string,
components: string,
woosmap_key: string,
): Promise<woosmap.map.localities.LocalitiesAutocompleteResponse> {
const args = {
key: woosmap_key,
input,
types: "locality|postal_code|address",
components: "country:fr|country:gb|country:it|country:es|country:de",
};

if (components !== "") {
if (args["components"]) {
args["components"] = components;
}
}
return fetch(
`https://api.woosmap.com/localities/autocomplete/?${buildQueryString(args)}`,
).then((response) => response.json());
}
function buildQueryString(params: object) {
const queryStringParts = [];

for (const key in params) {
if (params[key]) {
const value = params[key];
queryStringParts.push(
`${encodeURIComponent(key)}=${encodeURIComponent(value)}` as never,
);
}
}
return queryStringParts.join("&");
}
type DebouncePromiseFunction<T, Args extends any[]> = (
...args: Args
) => Promise<T>;

function debouncePromise<T, Args extends any[]>(
fn: (...args: Args) => Promise<T>,
delay: number,
): DebouncePromiseFunction<T, Args> {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let latestResolve: ((value: T | PromiseLike<T>) => void) | null = null;
let latestReject: ((reason?: any) => void) | null = null;

return function (...args: Args): Promise<T> {
return new Promise<T>((resolve, reject) => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
latestResolve = resolve;
latestReject = reject;
timeoutId = setTimeout(() => {
fn(...args)
.then((result) => {
if (latestResolve === resolve && latestReject === reject) {
resolve(result);
}
})
.catch((error) => {
if (latestResolve === resolve && latestReject === reject) {
reject(error);
}
});
}, delay);
});
};
}

init();

declare global {
interface Window {
init: () => void;
}
}
window.init = init;
// [END woosmap_localities_api_autocomplete_advanced]

export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"title": "Localities Api - Autocomplete (Country restrictions, address type in suggestions)",
"description": "Localities Autocomplete requests on locality, postal code and address types in UK, FR, IT, SP and DE",
"tag": "localities_api_autocomplete_advanced",
"name": "localities-api-autocomplete-advanced",
"pagination": {
"data": "mode",
"size": 1,
"alias": "mode"
},
"permalink": "samples/{{ page.fileSlug }}/{{mode}}/{% if mode == 'jsfiddle' %}demo{% else %}index{% endif %}.{{ page.outputFileExtension }}"
}
15 changes: 15 additions & 0 deletions samples/localities-api-autocomplete-advanced/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@use 'sass:meta'; // To enable @use via meta.load-css and keep comments in order

/* [START woosmap_localities_api_autocomplete_advanced] */
@include meta.load-css("../../shared/scss/_default.scss");
@include meta.load-css("../../shared/scss/_autocomplete_input.scss");
@include meta.load-css("../../shared/scss/_autocomplete_list.scss");

#response-container {
margin-top: 70px;
padding: 10px;
}
.bold {
font-weight: 700;
}
/* [END woosmap_localities_api_autocomplete_advanced] */
22 changes: 22 additions & 0 deletions samples/localities-api-autocomplete/index.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends '../../src/_includes/layout.njk' %}
{% block html %}
<!-- [START woosmap_{{ tag }}_div] -->
<div id="app">
<div id="autocomplete-container">
{% svgIcon 'search.svg' %}
<input
type="text"
id="autocomplete-input"
placeholder="Search a locality or a postal code..."
autocomplete="off"
/>
<button aria-label="Clear" class="clear-searchButton" type="button">
{% svgIcon 'clear.svg' %}
</button>
<ul id="suggestions-list"></ul>
</div>
<pre id="response-container"></pre>
</div>
<!-- [END woosmap_{{ tag }}_div] -->
{% endblock %}
{% block api %}{% endblock %}
Loading

0 comments on commit 59aaf9c

Please sign in to comment.