Skip to content

Commit b01d579

Browse files
authored
Merge pull request #64 from github/feat-add-loading-lazy
Feat add loading lazy
2 parents cb5cb5e + 3f2b6ff commit b01d579

File tree

4 files changed

+194
-7
lines changed

4 files changed

+194
-7
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ On page load, the `include-fragment` element fetches the URL, the response is pa
4040

4141
The server must respond with an HTML fragment to replace the `include-fragment` element. It should not contain _another_ `include-fragment` element or the server will be polled in an infinite loop.
4242

43+
### Other Attributes
44+
45+
#### accept
46+
47+
This attribute tells `<include-fragment/>` what to send as the `Accept` header, as part of the fetch request. If omitted, or if set to an empty value, the default behaviour will be `text/html`. It is important that the server responds with HTML, but you may wish to change the accept header to help negotiate the right content with the server.
48+
49+
#### loading
50+
51+
This indicates _when_ the contents should be fetched:
52+
53+
- `eager`: Fetches and load the content immediately, regardless of whether or not the `<include-fragment/>` is currently within the visible viewport (this is the default value).
54+
- `lazy`: Defers fetching and loading the content until the `<include-fragment/>` tag reaches a calculated distance from the viewport. The intent is to avoid the network and storage bandwidth needed to handle the content until it's reasonably certain that it will be needed.
55+
56+
The
57+
4358
### Errors
4459

4560
If the URL fails to load, the `include-fragment` element is left in the page and tagged with an `is-error` CSS class that can be used for styling.

examples/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@
88
<include-fragment src="./pull.html">Loading</include-fragment>
99
<!-- <script type="module" src="../dist/index.js"></script> -->
1010
<script type="module" src="https://unpkg.com/@github/include-fragment-element@latest?module"></script>
11+
<details>
12+
<summary>Click to unfold a lazy include-fragment</summary>
13+
<include-fragment src="./pull.html" loading="lazy">Loading</include-fragment>
14+
</details>
1115
</body>
1216
</html>

src/index.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
const privateData = new WeakMap()
22

3+
const observer = new IntersectionObserver(entries => {
4+
for(const entry of entries) {
5+
if (entry.isIntersecting) {
6+
const {target} = entry
7+
observer.unobserve(target)
8+
if (!(target instanceof IncludeFragmentElement)) return
9+
if (target.loading === 'lazy') {
10+
handleData(target)
11+
}
12+
}
13+
}
14+
}, {
15+
// Currently the threshold is set to 256px from the bottom of the viewport
16+
// with a threshold of 0.1. This means the element will not load until about
17+
// 2 keyboard-down-arrow presses away from being visible in the viewport,
18+
// giving us some time to fetch it before the contents are made visible
19+
rootMargin: '0px 0px 256px 0px',
20+
threshold: 0.01
21+
})
22+
23+
324
function fire(name: string, target: Element) {
425
setTimeout(function () {
526
target.dispatchEvent(new Event(name))
627
}, 0)
728
}
829

930
async function handleData(el: IncludeFragmentElement) {
31+
observer.unobserve(el)
1032
// eslint-disable-next-line github/no-then
1133
return getData(el).then(
1234
function (html: string) {
@@ -47,7 +69,7 @@ function isWildcard(accept: string | null) {
4769
export default class IncludeFragmentElement extends HTMLElement {
4870

4971
static get observedAttributes(): string[] {
50-
return ['src']
72+
return ['src', 'loading']
5173
}
5274

5375
get src(): string {
@@ -65,6 +87,15 @@ export default class IncludeFragmentElement extends HTMLElement {
6587
this.setAttribute('src', val)
6688
}
6789

90+
get loading(): 'eager'|'lazy' {
91+
if (this.getAttribute('loading') === 'lazy') return 'lazy'
92+
return 'eager'
93+
}
94+
95+
set loading(value: 'eager'|'lazy') {
96+
this.setAttribute('loading', value)
97+
}
98+
6899
get accept(): string {
69100
return this.getAttribute('accept') || ''
70101
}
@@ -77,19 +108,27 @@ export default class IncludeFragmentElement extends HTMLElement {
77108
return getData(this)
78109
}
79110

80-
attributeChangedCallback(attribute: string): void {
111+
attributeChangedCallback(attribute: string, oldVal:string|null): void {
81112
if (attribute === 'src') {
82113
// Source changed after attached so replace element.
83-
if (this.isConnected) {
114+
if (this.isConnected && this.loading === 'eager') {
115+
handleData(this)
116+
}
117+
} else if (attribute === 'loading') {
118+
// Loading mode changed to Eager after attached so replace element.
119+
if (this.isConnected && oldVal !== 'eager' && this.loading === 'eager') {
84120
handleData(this)
85121
}
86122
}
87123
}
88124

89125
connectedCallback(): void {
90-
if (this.src) {
126+
if (this.src && this.loading === 'eager') {
91127
handleData(this)
92128
}
129+
if (this.loading === 'lazy') {
130+
observer.observe(this)
131+
}
93132
}
94133

95134
request(): Request {
@@ -108,6 +147,7 @@ export default class IncludeFragmentElement extends HTMLElement {
108147
}
109148

110149
load(): Promise<string> {
150+
observer.unobserve(this)
111151
return Promise.resolve()
112152
.then(() => {
113153
fire('loadstart', this)

test/test.js

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const responses = {
1010
}
1111
})
1212
},
13+
'/slow-hello': function() {
14+
return new Promise(resolve => {
15+
setTimeout(resolve, 100)
16+
}).then(responses['/hello'])
17+
},
1318
'/one-two': function() {
1419
return new Response('<p id="one">one</p><p id="two">two</p>', {
1520
status: 200,
@@ -372,7 +377,7 @@ suite('include-fragment-element', function() {
372377
})
373378
})
374379

375-
test.only('fires replaced event', function() {
380+
test('fires replaced event', function() {
376381
const elem = document.createElement('include-fragment')
377382
document.body.appendChild(elem)
378383

@@ -386,7 +391,7 @@ suite('include-fragment-element', function() {
386391
})
387392
})
388393

389-
test.only('fires events for include-fragment node replacement operations for fragment manipulation', function() {
394+
test('fires events for include-fragment node replacement operations for fragment manipulation', function() {
390395
const elem = document.createElement('include-fragment')
391396
document.body.appendChild(elem)
392397

@@ -404,7 +409,7 @@ suite('include-fragment-element', function() {
404409
})
405410
})
406411

407-
test.only('does not replace node if event was canceled ', function() {
412+
test('does not replace node if event was canceled ', function() {
408413
const elem = document.createElement('include-fragment')
409414
document.body.appendChild(elem)
410415

@@ -420,4 +425,127 @@ suite('include-fragment-element', function() {
420425
assert(document.querySelector('include-fragment'), 'Node should not be replaced')
421426
})
422427
})
428+
429+
test('sets loading to "eager" by default', function() {
430+
const div = document.createElement('div')
431+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
432+
document.body.appendChild(div)
433+
434+
assert(div.firstChild.loading, 'eager')
435+
})
436+
437+
test('loading will return "eager" even if set to junk value', function() {
438+
const div = document.createElement('div')
439+
div.innerHTML = '<include-fragment loading="junk" src="/hello">loading</include-fragment>'
440+
document.body.appendChild(div)
441+
442+
assert(div.firstChild.loading, 'eager')
443+
})
444+
445+
test('loading=lazy loads if already visible on page', function() {
446+
const div = document.createElement('div')
447+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
448+
document.body.appendChild(div)
449+
450+
return when(div.firstChild, 'include-fragment-replaced').then(() => {
451+
assert.equal(document.querySelector('include-fragment'), null)
452+
assert.equal(document.querySelector('#replaced').textContent, 'hello')
453+
})
454+
})
455+
456+
test('loading=lazy does not load if not visible on page', function() {
457+
const div = document.createElement('div')
458+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
459+
div.hidden = true
460+
document.body.appendChild(div)
461+
return Promise.race([
462+
when(div.firstChild, 'load').then(() => {
463+
throw new Error('<include-fragment loading=lazy> loaded too early')
464+
}),
465+
new Promise(resolve => setTimeout(resolve, 100))
466+
])
467+
})
468+
469+
test('loading=lazy does not load when src is changed', function() {
470+
const div = document.createElement('div')
471+
div.innerHTML = '<include-fragment loading="lazy" src="">loading</include-fragment>'
472+
div.hidden = true
473+
document.body.appendChild(div)
474+
div.firstChild.src = '/hello'
475+
return Promise.race([
476+
when(div.firstChild, 'load').then(() => {
477+
throw new Error('<include-fragment loading=lazy> loaded too early')
478+
}),
479+
new Promise(resolve => setTimeout(resolve, 100))
480+
])
481+
})
482+
483+
484+
test('loading=lazy loads as soon as element visible on page', function() {
485+
const div = document.createElement('div')
486+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
487+
div.hidden = true
488+
let failed = false
489+
document.body.appendChild(div)
490+
const fail = () => failed = true
491+
div.firstChild.addEventListener('load', fail)
492+
493+
setTimeout(function() {
494+
div.hidden = false
495+
div.firstChild.removeEventListener('load', fail)
496+
}, 100)
497+
498+
return when(div.firstChild, 'load').then(() => {
499+
assert.ok(!failed, "Load occured too early")
500+
})
501+
})
502+
503+
test('loading=lazy does not observably change during load cycle', function() {
504+
const div = document.createElement('div')
505+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
506+
const elem = div.firstChild
507+
document.body.appendChild(div)
508+
509+
return when(elem, 'loadstart').then(() => {
510+
assert.equal(elem.loading, 'lazy', "loading mode changed observably")
511+
})
512+
})
513+
514+
test('loading=lazy can be switched to eager to load', function() {
515+
const div = document.createElement('div')
516+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
517+
div.hidden = true
518+
let failed = false
519+
document.body.appendChild(div)
520+
const fail = () => failed = true
521+
div.firstChild.addEventListener('load', fail)
522+
523+
setTimeout(function() {
524+
div.firstChild.loading = 'eager'
525+
div.firstChild.removeEventListener('load', fail)
526+
}, 100)
527+
528+
return when(div.firstChild, 'load').then(() => {
529+
assert.ok(!failed, "Load occured too early")
530+
})
531+
})
532+
533+
test('loading=lazy wont load twice even if load is manually called', function() {
534+
const div = document.createElement('div')
535+
div.innerHTML = '<include-fragment loading="lazy" src="/slow-hello">loading</include-fragment>'
536+
div.hidden = true
537+
document.body.appendChild(div)
538+
let count = 0
539+
div.firstChild.addEventListener('loadstart', () => count += 1)
540+
const load = div.firstChild.load()
541+
setTimeout(() => {
542+
div.hidden = false
543+
}, 0)
544+
545+
return load
546+
.then(() => when(div.firstChild, 'loadend'))
547+
.then(() => {
548+
assert.equal(count, 1, "Load occured too many times")
549+
})
550+
})
423551
})

0 commit comments

Comments
 (0)