Skip to content
This repository has been archived by the owner on Feb 13, 2019. It is now read-only.

<script is="lazy-template"> (ready for review) #165

Merged
merged 6 commits into from Sep 30, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions elements/lazy-template/lazy-template-examples.js
@@ -0,0 +1,37 @@
export default {
element: 'lazy-template',
examples: {
'Load On Page Load': {
render () {
return `
<script type="text/html" is="lazy-template" load-on="page-load">
<h1>Damn Son, I Am Lazy Loaded</h1>
<img src="http://www.fillmurray.com/g/200/300" alt="Billy Murray">
</script>
`;
},
},
'Load On In View': {
render () {
return `
<div
style="
overflow: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
"
>
<div style="height: 200%">SCROLL DOWN TO LOAD STUFF</div>
<script type="text/html" is="lazy-template" load-on="in-view">
<h1>Damn Son, I Am Lazy Loaded</h1>
<img src="http://www.fillmurray.com/g/200/300" alt="Billy Murray">
</script>
</div>
`;
},
},
},
};
75 changes: 75 additions & 0 deletions elements/lazy-template/lazy-template.js
@@ -0,0 +1,75 @@
import { registerElement } from 'bulbs-elements/register';
import { InViewMonitor } from 'bulbs-elements/util';
import invariant from 'invariant';
import './lazy-template.scss';

function BulbsHTMLScriptElement () {}
BulbsHTMLScriptElement.prototype = HTMLScriptElement.prototype;

class LazyTemplate extends BulbsHTMLScriptElement {
get loadOn () {
return this.getAttribute('load-on');
}

attachedCallback () {
invariant(this.hasAttribute('load-on'),
'<script is="lazy-template"> MUST specify a "load-on" attribute (either "page-load" or "in-view").');

invariant(this.getAttribute('type') === 'text/html',
'<script is="lazy-template"> MUST set the attribute type="text/html".');

if (this.loadOn === 'page-load') {
this.setupLoadOnPageLoad();
}
else if (this.loadOn === 'in-view') {
this.setUpLoadOnInView();
}

this.replaceWithContents = this.replaceWithContents.bind(this);
this.handleEnterViewport = this.handleEnterViewport.bind(this);
}

detachedCallback () {
if (this.loadOn === 'page-load') {
this.tearDownLoadOnPageLoad();
}
else if (this.loadOn === 'in-view') {
this.tearDownLoadOnInView();
}

}

setupLoadOnPageLoad () {
if (document.readyState === 'complete') {
this.replaceWithContents();
}
else {
window.addEventListener('load', () => this.replaceWithContents());
}
}

setUpLoadOnInView () {
InViewMonitor.add(this);
this.addEventListener('enterviewport', this.handleEnterViewport);
}

tearDownLoadOnPageLoad () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to leave this here if it's a no-op?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

}

handleEnterViewport () {
InViewMonitor.remove(this);
this.replaceWithContents();
}

tearDownLoadOnInView () {
InViewMonitor.remove(this);
}

replaceWithContents () {
this.outerHTML = this.textContent;
}
}

LazyTemplate.extends = 'script';

registerElement('lazy-template', LazyTemplate);
11 changes: 11 additions & 0 deletions elements/lazy-template/lazy-template.scss
@@ -0,0 +1,11 @@
// we're using getBoundingClientRect to determine when our script
// is in-view. Typically scripts are not part of the render tree
// and their client rect is not meaningful. We can force them to be
// an invisible 0x0 block element like this. Then we can meaninfully
// track their position.
script[is="lazy-template"] {
display: block;
width: 0px;
height: 0px;
overflow: hidden;
}
97 changes: 97 additions & 0 deletions elements/lazy-template/lazy-template.test.js
@@ -0,0 +1,97 @@
import './lazy-template';

describe.only('<script is="lazy-template">', () => {
let sandbox;
let subject;
let container;

beforeEach(() => {
sandbox = sinon.sandbox.create();
subject = makeTemplate(`
<h1>Cool Template</h1>
<p>It is lazy!</p>
`);
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
sandbox.restore();
container.remove();
});

function makeTemplate (innerHTML) {
let template = document.createElement('script', 'lazy-template');
template.setAttribute('type', 'text/html');
template.setAttribute('load-on', 'page-load');
template.innerHTML = innerHTML;
return template;
}

it('requires a load-on attribute', () => {
subject.removeAttribute('load-on');
expect(() => {
subject.attachedCallback();
}).to.throw('<script is="lazy-template"> MUST specify a "load-on" attribute (either "page-load" or "in-view").');
});

it('requires a type set to "text/html"', () => {
subject.setAttribute('type', 'text/whatever');
expect(() => {
subject.attachedCallback();
}).to.throw('<script is="lazy-template"> MUST set the attribute type="text/html".');
});

it('requires a type attribute', () => {
subject.removeAttribute('type');
expect(() => {
subject.attachedCallback();
}).to.throw('<script is="lazy-template"> MUST set the attribute type="text/html".');
});

describe('replaceWithContents', () => {
it('replaces itself with the content html', () => {
container = document.createElement('div');
container.appendChild(subject);
subject.replaceWithContents();

expect(container.children[0].outerHTML).to.eql('<h1>Cool Template</h1>');
expect(container.children[1].outerHTML).to.eql('<p>It is lazy!</p>');
});
});

context('load-on="page-load"', () => {
beforeEach(() => subject.setAttribute('load-on', 'page-load'));

xit('sets content when page load event fires', () => {
// document.readyState is 'complete' by the time this test starts
// and it can't be overwritten, not sure how to test this
});

it('sets content immediately if page load event has fired', (done) => {
container.appendChild(subject);

setImmediate(() => {
expect(container.children[0].outerHTML).to.eql('<h1>Cool Template</h1>');
expect(container.children[1].outerHTML).to.eql('<p>It is lazy!</p>');
done();
});
});
});

context('load-on="in-view"', () => {
beforeEach(() => subject.setAttribute('load-on', 'in-view'));

it('sets content when enterviewport event fires', (done) => {
container.appendChild(subject);

setImmediate(() => {
expect(container.children[0].tagName).to.eql('SCRIPT');
subject.dispatchEvent(new CustomEvent('enterviewport'));
expect(container.children[0].outerHTML).to.eql('<h1>Cool Template</h1>');
expect(container.children[1].outerHTML).to.eql('<p>It is lazy!</p>');
done();
});
});
});
});