Skip to content

Commit 8808728

Browse files
committed
feat: MySearchApiElement - <my-search-api> (#66)
* nfreear/diveintoaccessibility#19
1 parent cbaea16 commit 8808728

2 files changed

Lines changed: 150 additions & 0 deletions

File tree

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export { MyGtagElement } from './src/components/MyGtagElement.js';
3030
export { MyOpenBadgeElement } from './src/components/MyOpenBadgeElement.js';
3131
export { MySharingWidgetElement } from './src/components/MySharingWidgetElement.js';
3232
export { MyZoomElement } from './src/components/MyZoomElement.js';
33+
export { MySearchApiElement } from './src/components/MySearchApiElement.js';
3334

3435
export { MyMinElement } from './src/MyMinElement.js';
3536
export { translate } from './src/translate.js';
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
const { fetch, HTMLElement } = window;
2+
3+
/**
4+
* Programmable Search Engine
5+
*
6+
* @see https://developers.google.com/custom-search/v1/overview#
7+
* @see https://programmablesearchengine.google.com/controlpanel/all
8+
* @customElement my-search-api
9+
*/
10+
export class MySearchApiElement extends HTMLElement {
11+
static getTag () { return 'my-search-api'; }
12+
13+
#form;
14+
#resultElem;
15+
#data;
16+
#response;
17+
18+
get #searchId () { return this.getAttribute('search-id'); }
19+
// https://developers.google.com/custom-search/v1/introduction#identify_your_application_to_google_with_api_key
20+
get #apiKey () { return this.getAttribute('key'); }
21+
get #buttonText () { return this.getAttribute('button-text') ?? 'Search'; }
22+
23+
#searchUrl (query) {
24+
const encq = encodeURIComponent(query);
25+
return `https://customsearch.googleapis.com/customsearch/v1?key=${this.#apiKey}&cx=${this.#searchId}&q=${encq}`;
26+
}
27+
28+
#assertRequired () {
29+
console.assert(this.#searchId, 'search-id is required');
30+
console.assert(this.#apiKey, 'api-key is required');
31+
console.assert(this.#buttonText, 'button-text is required');
32+
}
33+
34+
connectedCallback () {
35+
this.#assertRequired();
36+
const root = this.attachShadow({ mode: 'open' });
37+
38+
// const ctr = document.querySelector('#searchID');
39+
40+
const { form, results } = this.#createElements();
41+
this.#form = form;
42+
this.#resultElem = results;
43+
44+
root.appendChild(form);
45+
root.appendChild(results);
46+
47+
this.#form.addEventListener('submit', async (ev) => this.#submitHandler(ev));
48+
49+
console.debug('search-api', [this]);
50+
}
51+
52+
async #submitHandler (ev) {
53+
this.dataset.loading = true;
54+
ev.preventDefault();
55+
56+
const query = ev.target.elements.query.value;
57+
58+
const { items } = await this.#fetchResults(query);
59+
const elems = items.map((it) => this.#createListItem(it));
60+
61+
elems.forEach((el) => { this.#resultElem.appendChild(el); });
62+
63+
this.dataset.loading = false;
64+
}
65+
66+
async #fetchResults (query) {
67+
const resp = this.#response = await fetch(this.#searchUrl(query));
68+
this.dataset.query = query;
69+
this.dataset.httpStatus = resp.status;
70+
71+
if (!resp.ok) {
72+
console.error('Fetch Error:', resp.status, resp.url, resp); // 400 Bad Request.
73+
return { items: [], resp };
74+
}
75+
this.#data = await resp.json();
76+
const { context, items, kind, queries, searchInformation } = this.#data;
77+
78+
console.debug('Fetch OK:', context, items, kind, queries, searchInformation, resp, [this]);
79+
80+
this.dataset.count = items.length;
81+
82+
return { context, items, kind, queries, searchInformation, resp };
83+
}
84+
85+
#createListItem (it) {
86+
const { link, htmlSnippet, htmlTitle } = it;
87+
// Was: const { link, snippet, title, htmlSnippet, htmlTitle, pagemap }
88+
const li = document.createElement('li');
89+
const anchor = document.createElement('a');
90+
const para = document.createElement('p');
91+
92+
anchor.setAttribute('part', 'a');
93+
anchor.href = link;
94+
// anchor.textContent = title;
95+
anchor.innerHTML = htmlTitle;
96+
para.innerHTML = htmlSnippet;
97+
para.setAttribute('part', 'p');
98+
li.setAttribute('part', 'li');
99+
li.appendChild(anchor);
100+
li.appendChild(para);
101+
102+
return li;
103+
}
104+
105+
#createElements () {
106+
const form = document.createElement('form');
107+
const results = document.createElement('ul');
108+
const labelElem = document.createElement('label');
109+
const inputElem = document.createElement('input');
110+
const outputElem = document.createElement('output');
111+
const buttonElem = document.createElement('button');
112+
const buttonTextElem = document.createElement('span');
113+
const slotElem = document.createElement('slot');
114+
115+
form.appendChild(labelElem);
116+
form.appendChild(inputElem);
117+
form.appendChild(buttonElem);
118+
form.appendChild(outputElem);
119+
120+
buttonElem.appendChild(buttonTextElem);
121+
labelElem.appendChild(slotElem);
122+
123+
slotElem.textContent = 'Search';
124+
buttonTextElem.textContent = this.#buttonText;
125+
126+
buttonElem.setAttribute('aria-label', this.#buttonText);
127+
128+
labelElem.setAttribute('part', 'label');
129+
inputElem.setAttribute('part', 'input');
130+
buttonElem.setAttribute('part', 'button');
131+
buttonTextElem.setAttribute('part', 'buttonText');
132+
outputElem.setAttribute('part', 'output');
133+
results.setAttribute('part', 'ul results');
134+
135+
labelElem.setAttribute('for', 'q');
136+
inputElem.id = 'q';
137+
inputElem.type = 'search';
138+
inputElem.name = 'query';
139+
outputElem.name = 'output';
140+
141+
inputElem.setAttribute('required', '');
142+
inputElem.setAttribute('minlength', 2);
143+
inputElem.setAttribute('maxlength', 40);
144+
145+
return { form, results };
146+
}
147+
}
148+
149+
export default MySearchApiElement;

0 commit comments

Comments
 (0)