Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@
function fakeFetch(url) {
const urlObj = new URL(url)
let html = ''
if (url.pathname === '/results') {
if (urlObj.pathname === '/results') {
const doc = document.createElement('div')
doc.innerHTML = `<li>Hubot</li><li>BB-8</li><li>Wall-E</li><li>Bender</li>`
const q = url.searchParams.get('q')
const q = urlObj.searchParams.get('q')
for (const el of doc.querySelectorAll('li')) {
if (q !== '' && !el.textContent.toLowerCase().match(q.toLowerCase())) el.remove()
}
html = doc.innerHTML
} else if (url.pathname === '/marquee') {
html = `<marquee>${url.searchParams.get('q') || '🐈 Nothing to preview 🐈'}</marquee>`
} else if (urlObj.pathname === '/marquee') {
html = `<marquee>${urlObj.searchParams.get('q') || '🐈 Nothing to preview 🐈'}</marquee>`
}
const promiseHTML = new Promise(resolve => resolve(html))
return new Promise(resolve => resolve({ok: true, text: () => promiseHTML}))
return Promise.resolve({ok: true, text: () => Promise.resolve(html)})
}
window.fetch = fakeFetch
</script>
Expand All @@ -40,9 +39,9 @@
<ul id="results"></ul>

<!-- GitHub Pages development script, uncomment when running example locally and comment out the production one -->
<!-- <script src="../dist/index.umd.js"></script> -->
<!-- <script type="module" src="../dist/index.js"></script> -->

<!-- GitHub Pages demo script -->
<script src="https://unpkg.com/@github/remote-input-element@latest/dist/index.umd.js"></script>
<script type="module" src="https://unpkg.com/@github/remote-input-element@latest/dist/index.js"></script>
</body>
</html>
53 changes: 40 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class RemoteInputElement extends HTMLElement {
constructor() {
super()
const fetch = fetchResults.bind(null, this, true)
const state = {currentQuery: null, oninput: debounce(fetch), fetch}
const state = {currentQuery: null, oninput: debounce(fetch), fetch, controller: null}
states.set(this, state)
}

Expand Down Expand Up @@ -59,6 +59,13 @@ class RemoteInputElement extends HTMLElement {
}
}

function makeAbortController() {
if ('AbortController' in window) {
return new AbortController()
}
return {signal: null, abort() {}}
}

async function fetchResults(remoteInput: RemoteInputElement, checkCurrentQuery: boolean) {
const input = remoteInput.input
if (!input) return
Expand All @@ -82,33 +89,53 @@ async function fetchResults(remoteInput: RemoteInputElement, checkCurrentQuery:
params.append(remoteInput.getAttribute('param') || 'q', query)
url.search = params.toString()

remoteInput.dispatchEvent(new CustomEvent('loadstart'))
remoteInput.setAttribute('loading', '')
if (state.controller) {
state.controller.abort()
} else {
remoteInput.dispatchEvent(new CustomEvent('loadstart'))
remoteInput.setAttribute('loading', '')
}

state.controller = makeAbortController()

let response
let errored = false
let html = ''
try {
response = await fetch(url.toString(), {
response = await fetchWithNetworkEvents(remoteInput, url.toString(), {
signal: state.controller.signal,
credentials: 'same-origin',
headers: {accept: 'text/html; fragment'}
})
html = await response.text()
remoteInput.dispatchEvent(new CustomEvent('load'))
} catch {
errored = true
remoteInput.dispatchEvent(new CustomEvent('error'))
remoteInput.removeAttribute('loading')
} catch (error) {
if (error.name !== 'AbortError') {
remoteInput.removeAttribute('loading')
}
return
}
remoteInput.removeAttribute('loading')
if (errored) return

if (response && response.ok) {
remoteInput.dispatchEvent(new CustomEvent('remote-input-success', {bubbles: true}))
resultsContainer.innerHTML = html
remoteInput.dispatchEvent(new CustomEvent('remote-input-success', {bubbles: true}))
} else {
remoteInput.dispatchEvent(new CustomEvent('remote-input-error', {bubbles: true}))
}
}

remoteInput.dispatchEvent(new CustomEvent('loadend'))
async function fetchWithNetworkEvents(el: Element, url: string, options: RequestInit): Promise<Response> {
try {
const response = await fetch(url, options)
el.dispatchEvent(new CustomEvent('load'))
el.dispatchEvent(new CustomEvent('loadend'))
return response
} catch (error) {
if (error.name !== 'AbortError') {
el.dispatchEvent(new CustomEvent('error'))
el.dispatchEvent(new CustomEvent('loadend'))
}
throw error
}
}

function debounce(callback: () => void) {
Expand Down
169 changes: 96 additions & 73 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,107 +12,130 @@ describe('remote-input', function() {
})

describe('after tree insertion', function() {
let remoteInput
let input
let results

beforeEach(function() {
document.body.innerHTML = `
<remote-input aria-owns="results" src="/results">
<input>
</remote-input>
<div id="results"></div>
`
remoteInput = document.querySelector('remote-input')
input = remoteInput.querySelector('input')
results = document.querySelector('#results')
})

afterEach(function() {
document.body.innerHTML = ''
remoteInput = null
input = null
results = null
})

it('loads content', function(done) {
const remoteInput = document.querySelector('remote-input')
const input = document.querySelector('input')
const results = document.querySelector('#results')
it('emits network events in order', async function() {
const events = []
const track = event => events.push(event.type)

remoteInput.addEventListener('loadstart', track)
remoteInput.addEventListener('load', track)
remoteInput.addEventListener('loadend', track)

const completed = Promise.all([
once(remoteInput, 'loadstart'),
once(remoteInput, 'load'),
once(remoteInput, 'loadend')
])
changeValue(input, 'test')
await completed

assert.deepEqual(['loadstart', 'load', 'loadend'], events)
})

it('loads content', async function() {
assert.equal(results.innerHTML, '')
let successEvent = false
remoteInput.addEventListener('remote-input-success', function() {
successEvent = true
})
remoteInput.addEventListener('loadend', function() {
assert.ok(successEvent, 'success event happened')
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
done()
})
input.value = 'test'
input.focus()

const success = once(remoteInput, 'remote-input-success')
const loadend = once(remoteInput, 'loadend')

changeValue(input, 'test')

await success
await loadend
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')
})

it('handles not ok responses', function(done) {
const remoteInput = document.querySelector('remote-input')
const input = document.querySelector('input')
const results = document.querySelector('#results')
it('handles not ok responses', async function() {
remoteInput.src = '/500'
assert.equal(results.innerHTML, '')
let errorEvent = false
remoteInput.addEventListener('remote-input-error', function() {
errorEvent = true
})
remoteInput.addEventListener('loadend', function() {
assert.ok(errorEvent, 'error event happened')
assert.equal(results.innerHTML, '', 'nothing was appended')
done()
})
input.value = 'test'
input.focus()

const error = once(remoteInput, 'remote-input-error')
const loadend = once(remoteInput, 'loadend')

changeValue(input, 'test')

await loadend
await error

assert.equal(results.innerHTML, '', 'nothing was appended')
})

it('handles network error', function(done) {
const remoteInput = document.querySelector('remote-input')
const input = document.querySelector('input')
const results = document.querySelector('#results')
it('handles network error', async function() {
remoteInput.src = '/network-error'
assert.equal(results.innerHTML, '')
remoteInput.addEventListener('error', async function() {
await Promise.resolve()
assert.equal(results.innerHTML, '', 'nothing was appended')
assert.notOk(remoteInput.hasAttribute('loading'), 'loading attribute was removed')
done()
})
input.value = 'test'
input.focus()
assert.ok(remoteInput.hasAttribute('loading'), 'loading attribute was added')

const result = once(remoteInput, 'error')

changeValue(input, 'test')
assert.ok(remoteInput.hasAttribute('loading'), 'loading attribute should have been added')

await result
await nextTick()
assert.equal(results.innerHTML, '', 'nothing was appended')
assert.notOk(remoteInput.hasAttribute('loading'), 'loading attribute should have been removed')
})

it('repects param attribute', function(done) {
const remoteInput = document.querySelector('remote-input')
const input = document.querySelector('input')
const results = document.querySelector('#results')
it('repects param attribute', async function() {
remoteInput.setAttribute('param', 'robot')
assert.equal(results.innerHTML, '')
remoteInput.addEventListener('loadend', function() {
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?robot=test')
done()
})
input.value = 'test'
input.focus()

const result = once(remoteInput, 'remote-input-success')

changeValue(input, 'test')

await result
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?robot=test')
})

it('loads content again after src is changed', function(done) {
const remoteInput = document.querySelector('remote-input')
const input = document.querySelector('input')
const results = document.querySelector('#results')

function listenOnce(cb) {
remoteInput.addEventListener('loadend', cb, {once: true})
}
listenOnce(function() {
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')

listenOnce(function() {
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/srcChanged?q=test')
done()
})

remoteInput.src = '/srcChanged'
})
input.value = 'test'
input.focus()
it('loads content again after src is changed', async function() {
const result1 = once(remoteInput, 'remote-input-success')
changeValue(input, 'test')

await result1
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/results?q=test')

const result2 = once(remoteInput, 'remote-input-success')
remoteInput.src = '/srcChanged'

await result2
assert.equal(results.querySelector('ol').getAttribute('data-src'), '/srcChanged?q=test')
})
})
})

function changeValue(input, value) {
input.value = value
input.dispatchEvent(new Event('change'))
}

function nextTick() {
return Promise.resolve()
}

function once(element, eventName) {
return new Promise(resolve => {
element.addEventListener(eventName, resolve, {once: true})
})
}