Skip to content

Commit

Permalink
feat(lit-helpers): add live directive
Browse files Browse the repository at this point in the history
  • Loading branch information
LarsDenBakker committed Dec 1, 2019
1 parent 7f0c0c3 commit 1079b0f
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 0 deletions.
14 changes: 14 additions & 0 deletions packages/lit-helpers/README.md
Expand Up @@ -95,6 +95,20 @@ renderSpread({ foo: 'buz' });
renderSpread({ foo: undefined' });
```
## Live binding
For efficiency, lit-html does not set properties or attributes if they did not change since the previous render. This can cause problems when the properties are changed outside of lit-html's control. The `live` directive can be used to dirty check the element's live value, and set the property or attribute if it changed.
A great example for this, is the DOM element's `scrollTop` property which changes without lit-html knowing about it when the user scrolls.

By using the `live` directive, you can make sure it is always in sync with the value renderd by `lit-html`:

```js
html`
<my-element .scrollTop=${live(scrollTop)}></my-element>
`;
```

<script>
export default {
mounted() {
Expand Down
1 change: 1 addition & 0 deletions packages/lit-helpers/index.js
@@ -1,2 +1,3 @@
export { live } from './src/live.js';
export { spread } from './src/spread.js';
export { spreadProps } from './src/spreadProps.js';
32 changes: 32 additions & 0 deletions packages/lit-helpers/src/live.js
@@ -0,0 +1,32 @@
/* eslint-disable no-param-reassign, no-restricted-syntax, guard-for-in */
import { directive, PropertyPart, AttributePart } from 'lit-html';

export const live = directive((/** @type {unknown} */ value) => (
/** @type {PropertyPart | AttributePart} */ part,
) => {
const { element, name, strings } = part.committer;

if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') {
throw new Error('live directive bindings must not contain any static values');
}

if (part.committer.parts.length > 1) {
throw new Error('live directive must be the only directive for an attribute or property');
}

if (part instanceof PropertyPart) {
if (element[name] !== value) {
part.setValue(value);
}
return;
}

if (part instanceof AttributePart) {
if (element.getAttribute(name) !== value) {
part.setValue(value);
}
return;
}

throw new Error('live directive can only be used on attributes or properties');
});
132 changes: 132 additions & 0 deletions packages/lit-helpers/test/live.test.js
@@ -0,0 +1,132 @@
import { expect, html, fixture } from '@open-wc/testing';
import { spy } from 'sinon';
import { render } from 'lit-html';
import { live } from '../src/live.js';

class MyElement extends HTMLElement {
set myProp(value) {
this._myProp = value;
}

get myProp() {
return this._myProp;
}
}

customElements.define('my-element', MyElement);

describe('live', () => {
describe('property bindings', () => {
let wrapper;
beforeEach(async () => {
wrapper = await fixture(document.createElement('div'));
});

function renderLive(value) {
render(
html`
<my-element .myProp="${live(value)}"></my-element>
`,
wrapper,
);
return wrapper.firstElementChild;
}

it('can render a property', () => {
const element = renderLive('foo');
expect(element.myProp).to.equal('foo');
});

it('can change property values', () => {
const element = renderLive('foo');
renderLive('bar');
expect(element.myProp).to.equal('bar');
});

it('can render to null and undefined', () => {
const element = renderLive('foo');
renderLive(null);
expect(element.myProp).to.equal(null);
renderLive('bar');
expect(element.myProp).to.equal('bar');
renderLive(undefined);
expect(element.myProp).to.equal(undefined);
});

it('can change property values when the value on the element changes', () => {
const element = renderLive('foo');
element.myProp = 'bar';
renderLive('foo');

expect(element.myProp).to.equal('foo');
});

it('does not set property values when the value on the element did not change', () => {
const element = renderLive(undefined);
const myPropSpy = /** @type {*} */ (spy(element, 'myProp', ['get', 'set']));
renderLive('foo');
expect(myPropSpy.set.callCount).to.equal(1);
renderLive('bar');
expect(myPropSpy.set.callCount).to.equal(2);
renderLive('bar');
expect(myPropSpy.set.callCount).to.equal(2);
});
});

describe('attribute bindings', () => {
let wrapper;
beforeEach(async () => {
wrapper = await fixture(document.createElement('div'));
});

function renderLive(value) {
render(
html`
<div my-attr="${live(value)}"></div>
`,
wrapper,
);
return wrapper.firstElementChild;
}

it('can render an attribute', () => {
const element = renderLive('foo');
expect(element.getAttribute('my-attr')).to.equal('foo');
});

it('can change attribute values', () => {
const element = renderLive('foo');
renderLive('bar');
expect(element.getAttribute('my-attr')).to.equal('bar');
});

it('can render null and undefined', () => {
const element = renderLive('foo');
renderLive(null);
expect(element.getAttribute('my-attr')).to.equal('null');
renderLive('bar');
expect(element.getAttribute('my-attr')).to.equal('bar');
renderLive(undefined);
expect(element.getAttribute('my-attr')).to.equal('undefined');
});

it('can change attribute values when the value on the element changes', () => {
const element = renderLive('foo');
element.setAttribute('my-attr', 'bar');
renderLive('foo');

expect(element.getAttribute('my-attr')).to.equal('foo');
});

it('does not set attribute values when the value on the element did not change', () => {
const element = renderLive('');
const setAttribute = spy(element, 'setAttribute');
renderLive('foo');
expect(setAttribute.callCount).to.equal(1);
renderLive('bar');
expect(setAttribute.callCount).to.equal(2);
renderLive('bar');
expect(setAttribute.callCount).to.equal(2);
});
});
});

0 comments on commit 1079b0f

Please sign in to comment.