Skip to content

Commit 1594c38

Browse files
committed
feat: add loading=lazy
1 parent cb5cb5e commit 1594c38

File tree

2 files changed

+155
-6
lines changed

2 files changed

+155
-6
lines changed

src/index.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
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+
rootMargin: '0px 0px 256px 0px',
16+
threshold: 0.01
17+
})
18+
19+
320
function fire(name: string, target: Element) {
421
setTimeout(function () {
522
target.dispatchEvent(new Event(name))
623
}, 0)
724
}
825

926
async function handleData(el: IncludeFragmentElement) {
27+
observer.unobserve(el)
1028
// eslint-disable-next-line github/no-then
1129
return getData(el).then(
1230
function (html: string) {
@@ -47,7 +65,7 @@ function isWildcard(accept: string | null) {
4765
export default class IncludeFragmentElement extends HTMLElement {
4866

4967
static get observedAttributes(): string[] {
50-
return ['src']
68+
return ['src', 'loading']
5169
}
5270

5371
get src(): string {
@@ -65,6 +83,15 @@ export default class IncludeFragmentElement extends HTMLElement {
6583
this.setAttribute('src', val)
6684
}
6785

86+
get loading(): 'eager'|'lazy' {
87+
if (this.getAttribute('loading') === 'lazy') return 'lazy'
88+
return 'eager'
89+
}
90+
91+
set loading(value: 'eager'|'lazy') {
92+
this.setAttribute('loading', value)
93+
}
94+
6895
get accept(): string {
6996
return this.getAttribute('accept') || ''
7097
}
@@ -77,19 +104,27 @@ export default class IncludeFragmentElement extends HTMLElement {
77104
return getData(this)
78105
}
79106

80-
attributeChangedCallback(attribute: string): void {
107+
attributeChangedCallback(attribute: string, oldVal:string|null): void {
81108
if (attribute === 'src') {
82109
// Source changed after attached so replace element.
83110
if (this.isConnected) {
84111
handleData(this)
85112
}
113+
} else if (attribute === 'loading') {
114+
// Loading mode changed to Eager after attached so replace element.
115+
if (this.isConnected && oldVal !== 'eager' && this.loading === 'eager') {
116+
handleData(this)
117+
}
86118
}
87119
}
88120

89121
connectedCallback(): void {
90-
if (this.src) {
122+
if (this.src && this.loading === 'eager') {
91123
handleData(this)
92124
}
125+
if (this.loading === 'lazy') {
126+
observer.observe(this)
127+
}
93128
}
94129

95130
request(): Request {
@@ -108,6 +143,7 @@ export default class IncludeFragmentElement extends HTMLElement {
108143
}
109144

110145
load(): Promise<string> {
146+
observer.unobserve(this)
111147
return Promise.resolve()
112148
.then(() => {
113149
fire('loadstart', this)

test/test.js

Lines changed: 116 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,112 @@ 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 loads as soon as element visible on page', function() {
470+
const div = document.createElement('div')
471+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
472+
div.hidden = true
473+
let failed = false
474+
document.body.appendChild(div)
475+
const fail = () => failed = true
476+
div.firstChild.addEventListener('load', fail)
477+
478+
setTimeout(function() {
479+
div.hidden = false
480+
div.firstChild.removeEventListener('load', fail)
481+
}, 100)
482+
483+
return when(div.firstChild, 'load').then(() => {
484+
assert.ok(!failed, "Load occured too early")
485+
})
486+
})
487+
488+
test('loading=lazy does not observably change during load cycle', function() {
489+
const div = document.createElement('div')
490+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
491+
const elem = div.firstChild
492+
document.body.appendChild(div)
493+
494+
return when(elem, 'loadstart').then(() => {
495+
assert.equal(elem.loading, 'lazy', "loading mode changed observably")
496+
})
497+
})
498+
499+
test('loading=lazy can be switched to eager to load', function() {
500+
const div = document.createElement('div')
501+
div.innerHTML = '<include-fragment loading="lazy" src="/hello">loading</include-fragment>'
502+
div.hidden = true
503+
let failed = false
504+
document.body.appendChild(div)
505+
const fail = () => failed = true
506+
div.firstChild.addEventListener('load', fail)
507+
508+
setTimeout(function() {
509+
div.firstChild.loading = 'eager'
510+
div.firstChild.removeEventListener('load', fail)
511+
}, 100)
512+
513+
return when(div.firstChild, 'load').then(() => {
514+
assert.ok(!failed, "Load occured too early")
515+
})
516+
})
517+
518+
test('loading=lazy wont load twice even if load is manually called', function() {
519+
const div = document.createElement('div')
520+
div.innerHTML = '<include-fragment loading="lazy" src="/slow-hello">loading</include-fragment>'
521+
div.hidden = true
522+
document.body.appendChild(div)
523+
let count = 0
524+
div.firstChild.addEventListener('loadstart', () => count += 1)
525+
const load = div.firstChild.load()
526+
setTimeout(() => {
527+
div.hidden = false
528+
}, 0)
529+
530+
return load
531+
.then(() => when(div.firstChild, 'loadend'))
532+
.then(() => {
533+
assert.equal(count, 1, "Load occured too many times")
534+
})
535+
})
423536
})

0 commit comments

Comments
 (0)