Skip to content

Commit

Permalink
feat: implement custom event when location filter is submitted #70
Browse files Browse the repository at this point in the history
  • Loading branch information
fengelniederhammer committed Mar 18, 2024
1 parent f414205 commit 3b07d5b
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 31 deletions.
9 changes: 6 additions & 3 deletions components/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<html>
<head>
<meta charset="utf-8" />
<title>&lt;my-element> Demo</title>
<title>Components Demo</title>
<script src="./node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script src="./node_modules/lit/polyfill-support.js"></script>
<script type="module" src="./src/components/app.ts"></script>
Expand All @@ -12,7 +12,10 @@
</head>
<body>
<gs-app lapis="https://lapis.cov-spectrum.org/open/v1/sample">
<gs-location-filter value="Switzerland"></gs-location-filter>
<gs-location-filter
value="Europe / Switzerland"
fields='["region", "country", "division", "location"]'
></gs-location-filter>
<gs-prevalence-over-time
numerator='{"country":"Switzerland", "pangoLineage":"B.1.1.7", "dateTo":"2022-01-01"}'
denominator='{"country":"Switzerland", "dateTo":"2022-01-01"}'
Expand All @@ -30,7 +33,7 @@
</gs-app>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('gs-location-filter').addEventListener('location-changed', (event) => {
document.querySelector('gs-location-filter').addEventListener('gs-location-changed', (event) => {
const sequencesElements = document.querySelectorAll('gs-prevalence-over-time');
sequencesElements.forEach((el) => {
['numerator', 'denominator'].forEach((attr) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import '../app';
import './location-filter';
import data from './__mockData__/aggregated.json';
import { withinShadowRoot } from '../../storybook/withinShadowRoot.story';
import { expect, waitFor } from '@storybook/test';
import { expect, fn, userEvent, waitFor } from '@storybook/test';

const meta: Meta<{}> = {
title: 'Input/Location filter',
Expand All @@ -25,7 +25,9 @@ export default meta;
const Template: StoryObj<{ fields: string[] }> = {
render: (args) => {
return html` <gs-app lapis="${LAPIS_URL}">
<gs-location-filter .fields=${args.fields}></gs-location-filter>
<div class="max-w-screen-lg">
<gs-location-filter .fields=${args.fields}></gs-location-filter>
</div>
</gs-app>`;
},
args: {
Expand Down Expand Up @@ -105,3 +107,71 @@ export const FetchingLocationsFails: StoryObj<{ fields: string[] }> = {
await waitFor(() => expect(canvas.getByText('Error: TypeError', { exact: false })).toBeInTheDocument());
},
};

export const FiresEvent: StoryObj<{ fields: string[] }> = {
...Template,
parameters: {
fetchMock: {
mocks: [
{
matcher: aggregatedEndpointMatcher,
response: {
status: 200,
body: data,
},
},
],
},
},
play: async ({ canvasElement, step }) => {
const canvas = await withinShadowRoot(canvasElement, 'gs-location-filter');

const submitButton = () => canvas.getByRole('button', { name: 'Submit' });
const inputField = () => canvas.getByRole('combobox');

const listenerMock = fn();
await step('Setup event listener mock', async () => {
canvasElement.addEventListener('gs-location-changed', listenerMock);
});

await step('wait until data is loaded', async () => {
await waitFor(() => {
return expect(inputField()).toBeEnabled();
});
});

await step('Input invalid location', async () => {
await userEvent.type(inputField(), 'Not / A / Location');
await userEvent.click(submitButton());
await expect(listenerMock).not.toHaveBeenCalled();
await userEvent.type(inputField(), '{backspace>18/}');
});

await step('Select Asia', async () => {
await userEvent.type(inputField(), 'Asia');
await userEvent.click(submitButton());
await expect(listenerMock).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
region: 'Asia',
},
}),
);
});

await step('Select Asia / Bangladesh / Rajshahi / Chapainawabgonj', async () => {
await userEvent.type(inputField(), ' / Bangladesh / Rajshahi / Chapainawabgonj');
await userEvent.click(submitButton());
await expect(listenerMock).toHaveBeenCalledWith(
expect.objectContaining({
detail: {
region: 'Asia',
country: 'Bangladesh',
division: 'Rajshahi',
location: 'Chapainawabgonj',
},
}),
);
});
},
};
79 changes: 53 additions & 26 deletions components/src/components/locationFilter/location-filter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
import { Task } from '@lit/task';
import { consume } from '@lit/context';
import { lapisContext } from '../../lapis-context';
Expand All @@ -20,6 +20,9 @@ export class LocationFilter extends TailwindElement() {
@consume({ context: lapisContext })
lapis: string = '';

@state()
private unknownLocation = false;

private fetchingTask = new Task(this, {
task: async ([lapis], { signal }) => fetchAutocompletionList(this.fields, lapis, signal),
args: () => [this.lapis],
Expand All @@ -28,7 +31,7 @@ export class LocationFilter extends TailwindElement() {
override render() {
return this.fetchingTask.render({
pending: () => html`
<div class="flex">
<form class="flex">
<input
type="text"
class="input input-bordered flex-grow"
Expand All @@ -37,15 +40,15 @@ export class LocationFilter extends TailwindElement() {
disabled
/>
<button class="btn ml-1" disabled>Loading...</button>
</div>
</form>
`,
complete: (data) => html`
<div class="flex">
<form class="flex">
<input
type="text"
class="input input-bordered flex-grow"
class="input input-bordered flex-grow ${this.unknownLocation && 'border-2 border-error'}"
.value="${this.value}"
@input="${this.onInput}"
@input="${(event: Event) => this.onInput(event, data)}"
list="countries"
/>
<datalist id="countries">
Expand All @@ -59,35 +62,59 @@ export class LocationFilter extends TailwindElement() {
></option>`,
)}
</datalist>
<button class="btn btn-primary ml-1" @click="${() => this.submit(data)}">Submit</button>
</div>
<button
class="btn btn-primary ml-1"
@click="${(event: PointerEvent) => {
event.preventDefault();
this.submit(data);
}}"
>
Submit
</button>
</form>
`,
error: (e) => html`<p>Error: ${e}</p>`,
});
}

onInput(event: Event) {
private onInput(event: Event, data: Record<string, string>[]) {
const input = event.target as HTMLInputElement;
this.value = input.value;
if (this.unknownLocation) {
const eventDetail = this.parseLocation(this.value);
if (this.hasMatchingEntry(data, eventDetail)) {
this.unknownLocation = false;
}
}
}

submit(_data: unknown) {
// TODO #70
// if (data.regions.includes(this.value)) {
// this.dispatchEvent(
// new CustomEvent('gs-location-changed', {
// detail: { region: this.value, country: undefined },
// bubbles: true,
// }),
// );
// } else if (data.countries.includes(this.value)) {
// this.dispatchEvent(
// new CustomEvent('gs-location-changed', {
// detail: { region: undefined, country: this.value },
// bubbles: true,
// }),
// );
// }
private submit(data: Record<string, string>[]) {
const eventDetail = this.parseLocation(this.value);

if (this.hasMatchingEntry(data, eventDetail)) {
this.unknownLocation = false;
this.dispatchEvent(
new CustomEvent('gs-location-changed', {
detail: eventDetail,
bubbles: true,
}),
);
} else {
this.unknownLocation = true;
}
}

private parseLocation(location: string): Record<string, string> {
const fieldValues = location.split('/').map((part) => part.trim());
return fieldValues.reduce((acc, fieldValue, i) => ({ ...acc, [this.fields[i]]: fieldValue }), {});
}

private hasMatchingEntry(data: Record<string, string>[], eventDetail: Record<string, string>) {
const matchingEntries = Object.entries(eventDetail)
.filter(([, value]) => value !== undefined)
.reduce((filteredData, [key, value]) => filteredData.filter((it) => it[key] === value), data);

return matchingEntries.length > 0;
}
}

Expand Down

0 comments on commit 3b07d5b

Please sign in to comment.