Skip to content

Commit

Permalink
[lit-html] Add when, map, join, and range directives. (#2335)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinfagnani committed Dec 7, 2021
1 parent 49ecf62 commit d319cf5
Show file tree
Hide file tree
Showing 18 changed files with 432 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-guests-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lit-html': minor
---

Add `when`, `map`, `join`, and `range` directives.
16 changes: 16 additions & 0 deletions packages/lit-html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
"development": "./development/directives/if-defined.js",
"default": "./directives/if-defined.js"
},
"./directives/join.js": {
"development": "./development/directives/join.js",
"default": "./directives/join.js"
},
"./directives/map.js": {
"development": "./development/directives/map.js",
"default": "./directives/map.js"
},
"./directives/live.js": {
"development": "./development/directives/live.js",
"default": "./directives/live.js"
Expand All @@ -61,6 +69,10 @@
"development": "./development/directives/ref.js",
"default": "./directives/ref.js"
},
"./directives/range.js": {
"development": "./development/directives/range.js",
"default": "./directives/range.js"
},
"./directives/repeat.js": {
"development": "./development/directives/repeat.js",
"default": "./directives/repeat.js"
Expand All @@ -85,6 +97,10 @@
"development": "./development/directives/until.js",
"default": "./directives/until.js"
},
"./directives/when.js": {
"development": "./development/directives/when.js",
"default": "./directives/when.js"
},
"./experimental-hydrate.js": {
"development": "./development/experimental-hydrate.js",
"default": "./experimental-hydrate.js"
Expand Down
4 changes: 4 additions & 0 deletions packages/lit-html/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ export const defaultConfig = (options = {}) =>
'directives/class-map',
'directives/guard',
'directives/if-defined',
'directives/join',
'directives/live',
'directives/map',
'directives/range',
'directives/ref',
'directives/repeat',
'directives/style-map',
'directives/template-content',
'directives/unsafe-html',
'directives/unsafe-svg',
'directives/until',
'directives/when',
'lit-html',
'directive',
'directive-helpers',
Expand Down
40 changes: 40 additions & 0 deletions packages/lit-html/src/directives/join.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* Returns an iterable containing the values in `items` interleaved with the
* `joiner` value.
*
* @example
*
* ```ts
* render() {
* return html`
* ${join(items, html`<span class="separator">|</span>`)}
* `;
* }
*/
export function join<I, J>(
items: Iterable<I> | undefined,
joiner: (index: number) => J
): Iterable<I | J>;
export function join<I, J>(
items: Iterable<I> | undefined,
joiner: J
): Iterable<I | J>;
export function* join<I, J>(items: Iterable<I> | undefined, joiner: J) {
const isFunction = typeof joiner === 'function';
if (items !== undefined) {
let i = -1;
for (const value of items) {
if (i > -1) {
yield isFunction ? joiner(i) : joiner;
}
i++;
yield value;
}
}
}
33 changes: 33 additions & 0 deletions packages/lit-html/src/directives/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* Returns an iterable containing the result of calling `f(value)` on each
* value in `items`.
*
* @example
*
* ```ts
* render() {
* return html`
* <ul>
* ${map(items, (i) => html`<li>${i}</li>`)}
* </ul>
* `;
* }
* ```
*/
export function* map<T>(
items: Iterable<T> | undefined,
f: (value: T, index: number) => unknown
) {
if (items !== undefined) {
let i = 0;
for (const value of items) {
yield f(value, i++);
}
}
}
35 changes: 35 additions & 0 deletions packages/lit-html/src/directives/range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* Returns an iterable of integers from `start` to `end` (exclusive)
* incrementing by `step`.
*
* If `start` is omitted, the range starts at `0`. `step` defaults to `1`.
*
* @example
*
* ```ts
* render() {
* return html`
* ${map(range(8), () => html`<div class="cell"></div>`)}
* `;
* }
* ```
*/
export function range(end: number): Iterable<number>;
export function range(
start: number,
end: number,
step?: number
): Iterable<number>;
export function* range(startOrEnd: number, end?: number, step = 1) {
const start = end === undefined ? 0 : startOrEnd;
end ??= startOrEnd;
for (let i = start; step > 0 ? i < end : end < i; i += step) {
yield i;
}
}
45 changes: 45 additions & 0 deletions packages/lit-html/src/directives/when.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* When `condition` is true, returns the result of calling `trueCase()`, else
* returns the result of calling `falseCase()` if `falseCase` is defined.
*
* This is a convenience wrapper around a ternary expression that makes it a
* little nicer to write an inline conditional without an else.
*
* @example
*
* ```ts
* render() {
* return html`
* ${when(this.user, () => html`User: ${this.user.username}`, () => html`Sign In...`)}
* `;
* }
* ```
*/
export function when<T, F>(
condition: true,
trueCase: () => T,
falseCase?: () => F
): T;
export function when<T, F = undefined>(
condition: false,
trueCase: () => T,
falseCase?: () => F
): F;
export function when<T, F = undefined>(
condition: unknown,
trueCase: () => T,
falseCase?: () => F
): T | F;
export function when(
condition: unknown,
trueCase: () => unknown,
falseCase?: () => unknown
): unknown {
return condition ? trueCase() : falseCase?.();
}
47 changes: 47 additions & 0 deletions packages/lit-html/src/test/directives/join_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import {html} from '../../lit-html.js';
import {makeAssertRender} from '../test-utils/assert-render.js';

import {join} from '../../directives/join.js';

suite('join', () => {
let container: HTMLDivElement;

const assertRender = makeAssertRender(() => container);

setup(() => {
container = document.createElement('div');
});

test('with array', () => {
assertRender(join(['a', 'b', 'c'], ','), 'a,b,c');
});

test('with empty array', () => {
assertRender(join([], ','), '');
});

test('with undefined', () => {
assertRender(join(undefined, ','), '');
});

test('with iterable', () => {
function* iterate<T>(items: Array<T>) {
for (const i of items) {
yield i;
}
}
assertRender(join(iterate(['a', 'b', 'c']), ','), 'a,b,c');
});

test('passes index', () => {
assertRender(
join(['a', 'b', 'c'], (i) => html`<p>${i}</p>`),
'a<p>0</p>b<p>1</p>c'
);
});
});
58 changes: 58 additions & 0 deletions packages/lit-html/src/test/directives/map_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import {html} from '../../lit-html.js';
import {makeAssertRender} from '../test-utils/assert-render.js';

import {map} from '../../directives/map.js';

suite('map', () => {
let container: HTMLDivElement;

const assertRender = makeAssertRender(() => container);

setup(() => {
container = document.createElement('div');
});
test('with array', () => {
assertRender(
map(['a', 'b', 'c'], (v) => html`<p>${v}</p>`),
'<p>a</p><p>b</p><p>c</p>'
);
});

test('with empty array', () => {
assertRender(
map([], (v) => html`<p>${v}</p>`),
''
);
});

test('with undefined', () => {
assertRender(
map(undefined, (v) => html`<p>${v}</p>`),
''
);
});

test('with iterable', () => {
function* iterate<T>(items: Array<T>) {
for (const i of items) {
yield i;
}
}
assertRender(
map(iterate(['a', 'b', 'c']), (v) => html`<p>${v}</p>`),
'<p>a</p><p>b</p><p>c</p>'
);
});

test('passes index', () => {
assertRender(
map(['a', 'b', 'c'], (v, i) => html`<p>${v}:${i}</p>`),
'<p>a:0</p><p>b:1</p><p>c:2</p>'
);
});
});
35 changes: 35 additions & 0 deletions packages/lit-html/src/test/directives/range_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import {range} from '../../directives/range.js';
import {assert} from '@esm-bundle/chai';

suite('range', () => {
test('positive end', () => {
assert.deepEqual([...range(0)], []);
assert.deepEqual([...range(3)], [0, 1, 2]);
});

test('start and end', () => {
assert.deepEqual([...range(0, 3)], [0, 1, 2]);
assert.deepEqual([...range(-1, 1)], [-1, 0]);
assert.deepEqual([...range(-2, -1)], [-2]);
});

test('end < start', () => {
// This case checks that we don't cause an infinite loop
assert.deepEqual([...range(2, 1)], []);
});

test('custom step', () => {
assert.deepEqual([...range(0, 10, 3)], [0, 3, 6, 9]);
});

test('negative step', () => {
assert.deepEqual([...range(0, -3, -1)], [0, -1, -2]);
// This case checks that we don't cause an infinite loop
assert.deepEqual([...range(0, 10, -1)], []);
});
});

0 comments on commit d319cf5

Please sign in to comment.