Skip to content

Commit 5533a58

Browse files
committed
Final “2.x” interface.
* Removes “ifDefined” (previously just deprecated). * Removes “repeat” (previously just deprecated). Closes #314.
1 parent 95cacc7 commit 5533a58

File tree

5 files changed

+12
-507
lines changed

5 files changed

+12
-507
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4545
used to accomplish this behavior — https://github.com/whatwg/html/pull/1936.
4646
If integrators need this functionality in the interim, it can easily be copied
4747
into source for temporary use (#302).
48+
- The previously-deprecated `ifDefined` and `repeat` directives are formally
49+
removed and will cause a runtime error if you try to use them. Instead, use
50+
the new `??` binding and `[[]]`-entries for keyed mappings (#314).
4851

4952
### Fixed
5053

test/test-template-engine.js

Lines changed: 0 additions & 363 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,8 @@
11
import { assert, describe, it } from '@netflix/x-test/x-test.js';
22
import XElement from '../x-element.js';
33

4-
// Long-term interface.
54
const { render, html } = XElement.templateEngine;
65

7-
// Deprecated interface. We will eventually delete these.
8-
const { ifDefined, repeat } = XElement.templateEngine;
9-
10-
// Overwrite console warn for testing so we don’t get spammed with our own
11-
// deprecation warnings.
12-
const seen = new Set();
13-
const warn = console.warn; // eslint-disable-line no-console
14-
const localMessages = [
15-
'Deprecated "ifDefined" from default templating engine interface.',
16-
'Deprecated "repeat" from default templating engine interface.',
17-
];
18-
console.warn = (...args) => { // eslint-disable-line no-console
19-
if (!localMessages.includes(args[0]?.message)) {
20-
warn(...args);
21-
} else {
22-
seen.add(args[0].message);
23-
}
24-
};
25-
266
// Simple helper for asserting thrown messages.
277
const assertThrows = (callback, expectedMessage, options) => {
288
let thrown = false;
@@ -1169,346 +1149,3 @@ describe('value issues', () => {
11691149
});
11701150
});
11711151
});
1172-
1173-
describe('html updaters', () => {
1174-
it('ifDefined', () => {
1175-
const getTemplate = ({ maybe }) => {
1176-
return html`<div id="target" maybe="${ifDefined(maybe)}"></div>`;
1177-
};
1178-
const container = document.createElement('div');
1179-
document.body.append(container);
1180-
render(container, getTemplate({ maybe: 'yes' }));
1181-
assert(container.querySelector('#target').getAttribute('maybe') === 'yes');
1182-
render(container, getTemplate({ maybe: undefined }));
1183-
assert(container.querySelector('#target').getAttribute('maybe') === null);
1184-
render(container, getTemplate({ maybe: false }));
1185-
assert(container.querySelector('#target').getAttribute('maybe') === 'false');
1186-
render(container, getTemplate({ maybe: null }));
1187-
assert(container.querySelector('#target').getAttribute('maybe') === null);
1188-
container.remove();
1189-
});
1190-
1191-
// This is mainly for backwards compat, TBD if we deprecate or not.
1192-
it('repeat works when called with all arguments', () => {
1193-
const getTemplate = ({ items }) => {
1194-
return html`
1195-
<div id="target">
1196-
${repeat(items, item => item.id, item => {
1197-
return html`<div id="${item.id}" class="item">${item.id}</div>`;
1198-
})}
1199-
</div>
1200-
`;
1201-
};
1202-
const container = document.createElement('div');
1203-
document.body.append(container);
1204-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}, { id: 'baz' }] }));
1205-
const foo = container.querySelector('#foo');
1206-
const bar = container.querySelector('#bar');
1207-
const baz = container.querySelector('#baz');
1208-
assert(container.querySelector('#target').childElementCount === 3);
1209-
assert(!!foo);
1210-
assert(!!bar);
1211-
assert(!!baz);
1212-
assert(container.querySelector('#target').children[0] === foo);
1213-
assert(container.querySelector('#target').children[1] === bar);
1214-
assert(container.querySelector('#target').children[2] === baz);
1215-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}, { id: 'baz' }] }));
1216-
assert(container.querySelector('#target').childElementCount === 3);
1217-
assert(container.querySelector('#target').children[0] === foo);
1218-
assert(container.querySelector('#target').children[1] === bar);
1219-
assert(container.querySelector('#target').children[2] === baz);
1220-
render(container, getTemplate({ items: [{ id: 'baz' }, { id: 'foo' }, { id: 'bar'}] }));
1221-
assert(container.querySelector('#target').childElementCount === 3);
1222-
assert(container.querySelector('#target').children[0] === baz);
1223-
assert(container.querySelector('#target').children[1] === foo);
1224-
assert(container.querySelector('#target').children[2] === bar);
1225-
render(container, getTemplate({ items: [{ id: 'bar'}, { id: 'baz' }, { id: 'foo' }] }));
1226-
assert(container.querySelector('#target').childElementCount === 3);
1227-
assert(container.querySelector('#target').children[0] === bar);
1228-
assert(container.querySelector('#target').children[1] === baz);
1229-
assert(container.querySelector('#target').children[2] === foo);
1230-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}, { id: 'baz' }] }));
1231-
assert(container.querySelector('#target').childElementCount === 3);
1232-
assert(container.querySelector('#target').children[0] === foo);
1233-
assert(container.querySelector('#target').children[1] === bar);
1234-
assert(container.querySelector('#target').children[2] === baz);
1235-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}] }));
1236-
assert(container.querySelector('#target').childElementCount === 2);
1237-
assert(container.querySelector('#target').children[0] === foo);
1238-
assert(container.querySelector('#target').children[1] === bar);
1239-
render(container, getTemplate({ items: [{ id: 'foo' }] }));
1240-
assert(container.querySelector('#target').childElementCount === 1);
1241-
assert(container.querySelector('#target').children[0] === foo);
1242-
render(container, getTemplate({ items: [] }));
1243-
assert(container.querySelector('#target').childElementCount === 0);
1244-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}, { id: 'baz' }] }));
1245-
assert(container.querySelector('#target').childElementCount === 3);
1246-
assert(container.querySelector('#target').children[0] !== foo);
1247-
assert(container.querySelector('#target').children[1] !== bar);
1248-
assert(container.querySelector('#target').children[2] !== baz);
1249-
container.remove();
1250-
});
1251-
1252-
it('repeat works when called with omitted lookup', () => {
1253-
const getTemplate = ({ items }) => {
1254-
return html`
1255-
<div id="target">
1256-
${repeat(items, item => {
1257-
return html`<div id="${item.id}" class="item">${item.id}</div>`;
1258-
})}
1259-
</div>
1260-
`;
1261-
};
1262-
const container = document.createElement('div');
1263-
document.body.append(container);
1264-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}, { id: 'baz' }] }));
1265-
const foo = container.querySelector('#foo');
1266-
const bar = container.querySelector('#bar');
1267-
const baz = container.querySelector('#baz');
1268-
assert(container.querySelector('#target').childElementCount === 3);
1269-
assert(!!foo);
1270-
assert(!!bar);
1271-
assert(!!baz);
1272-
assert(container.querySelector('#target').children[0] === foo);
1273-
assert(container.querySelector('#target').children[1] === bar);
1274-
assert(container.querySelector('#target').children[2] === baz);
1275-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}, { id: 'baz' }] }));
1276-
assert(container.querySelector('#target').childElementCount === 3);
1277-
assert(container.querySelector('#target').children[0] === foo);
1278-
assert(container.querySelector('#target').children[1] === bar);
1279-
assert(container.querySelector('#target').children[2] === baz);
1280-
1281-
// Because "lookup" is omitted, we don't expect DOM nodes to remain after a shift.
1282-
render(container, getTemplate({ items: [{ id: 'baz' }, { id: 'foo' }, { id: 'bar'}] }));
1283-
assert(container.querySelector('#target').childElementCount === 3);
1284-
assert(container.querySelector('#target').children[0] !== baz);
1285-
assert(container.querySelector('#target').children[1] !== foo);
1286-
assert(container.querySelector('#target').children[2] !== bar);
1287-
container.remove();
1288-
});
1289-
1290-
it('repeat re-runs each time', () => {
1291-
const getTemplate = ({ items, lookup }) => {
1292-
return html`
1293-
<div>
1294-
<ul id="target">
1295-
${repeat(items, item => item.id, item => {
1296-
return html`<li id="${item.id}">${lookup?.[item.id]}</li>`;
1297-
})}
1298-
</ul>
1299-
</div>
1300-
`;
1301-
};
1302-
const container = document.createElement('div');
1303-
document.body.append(container);
1304-
const items = [{ id: 'a' }, { id: 'b'}, { id: 'c' }];
1305-
let lookup = { a: 'foo', b: 'bar', c: 'baz' };
1306-
render(container, getTemplate({ items, lookup }));
1307-
assert(container.querySelector('#target').childElementCount === 3);
1308-
assert(container.querySelector('#a').textContent === 'foo');
1309-
assert(container.querySelector('#b').textContent === 'bar');
1310-
assert(container.querySelector('#c').textContent === 'baz');
1311-
lookup = { a: 'fizzle', b: 'bop', c: 'fuzz' };
1312-
render(container, getTemplate({ items, lookup }));
1313-
assert(container.querySelector('#target').childElementCount === 3);
1314-
assert(container.querySelector('#a').textContent === 'fizzle');
1315-
assert(container.querySelector('#b').textContent === 'bop');
1316-
assert(container.querySelector('#c').textContent === 'fuzz');
1317-
container.remove();
1318-
});
1319-
1320-
it('repeat callbacks are provided args from underlying “.map” call', () => {
1321-
// The identify callback is written in a bizarre way to test that all the
1322-
// expected function arguments are actually passed in. If they weren’t you
1323-
// would get duplicated key errors or undefined key errors.
1324-
const getTemplate = ({ items }) => {
1325-
return html`
1326-
<div id="target">
1327-
${repeat(items, (_, index, array) => array?.[index]?.id, (_, index, array) => {
1328-
return html`<div id="${array?.[index]?.id}" class="item">${array?.[index]?.id}</div>`;
1329-
})}
1330-
</div>
1331-
`;
1332-
};
1333-
const container = document.createElement('div');
1334-
document.body.append(container);
1335-
render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar'}, { id: 'baz' }] }));
1336-
const foo = container.querySelector('#foo');
1337-
const bar = container.querySelector('#bar');
1338-
const baz = container.querySelector('#baz');
1339-
assert(container.querySelector('#target').childElementCount === 3);
1340-
assert(!!foo);
1341-
assert(!!bar);
1342-
assert(!!baz);
1343-
assert(container.querySelector('#target').children[0] === foo);
1344-
assert(container.querySelector('#target').children[1] === bar);
1345-
assert(container.querySelector('#target').children[2] === baz);
1346-
assert(foo.textContent === 'foo');
1347-
assert(bar.textContent === 'bar');
1348-
assert(baz.textContent === 'baz');
1349-
container.remove();
1350-
});
1351-
});
1352-
1353-
describe('updater errors', () => {
1354-
describe('ifDefined', () => {
1355-
it('throws if used on a "boolean"', () => {
1356-
const expected = 'The ifDefined update must be used on an attribute, not on a boolean attribute.';
1357-
const getTemplate = ({ maybe }) => {
1358-
return html`<div id="target" ?maybe="${ifDefined(maybe)}"></div>`;
1359-
};
1360-
const container = document.createElement('div');
1361-
document.body.append(container);
1362-
let actual;
1363-
try {
1364-
render(container, getTemplate({ maybe: 'yes' }));
1365-
} catch (error) {
1366-
actual = error.message;
1367-
}
1368-
assert(!!actual, 'No error was thrown.');
1369-
assert(actual === expected, actual);
1370-
container.remove();
1371-
});
1372-
1373-
it('throws if used on a "defined"', () => {
1374-
const expected = 'The ifDefined update must be used on an attribute, not on a defined attribute.';
1375-
const getTemplate = ({ maybe }) => {
1376-
return html`<div id="target" ??maybe="${ifDefined(maybe)}"></div>`;
1377-
};
1378-
const container = document.createElement('div');
1379-
document.body.append(container);
1380-
let actual;
1381-
try {
1382-
render(container, getTemplate({ maybe: 'yes' }));
1383-
} catch (error) {
1384-
actual = error.message;
1385-
}
1386-
assert(!!actual, 'No error was thrown.');
1387-
assert(actual === expected, actual);
1388-
container.remove();
1389-
});
1390-
1391-
it('throws if used on a "property"', () => {
1392-
const expected = 'The ifDefined update must be used on an attribute, not on a property.';
1393-
const getTemplate = ({ maybe }) => {
1394-
return html`<div id="target" .maybe="${ifDefined(maybe)}"></div>`;
1395-
};
1396-
const container = document.createElement('div');
1397-
document.body.append(container);
1398-
let actual;
1399-
try {
1400-
render(container, getTemplate({ maybe: 'yes' }));
1401-
} catch (error) {
1402-
actual = error.message;
1403-
}
1404-
assert(!!actual, 'No error was thrown.');
1405-
assert(actual === expected, actual);
1406-
container.remove();
1407-
});
1408-
});
1409-
1410-
describe('repeat', () => {
1411-
it('throws if callback is not a function (1)', () => {
1412-
const expected = 'Unexpected repeat identify "undefined" provided, expected a function.';
1413-
const getTemplate = ({ maybe }) => {
1414-
return html`<div id="target" maybe="${repeat(maybe)}"></div>`;
1415-
};
1416-
const container = document.createElement('div');
1417-
document.body.append(container);
1418-
let actual;
1419-
try {
1420-
render(container, getTemplate({ maybe: ['yes'] }));
1421-
} catch (error) {
1422-
actual = error.message;
1423-
}
1424-
assert(!!actual, 'No error was thrown.');
1425-
assert(actual === expected, actual);
1426-
container.remove();
1427-
});
1428-
1429-
it('throws if callback is not a function (2)', () => {
1430-
const expected = 'Unexpected repeat callback "5" provided, expected a function.';
1431-
const getTemplate = ({ maybe }) => {
1432-
return html`<div id="target" maybe="${repeat(maybe, 5)}"></div>`;
1433-
};
1434-
const container = document.createElement('div');
1435-
document.body.append(container);
1436-
let actual;
1437-
try {
1438-
render(container, getTemplate({ maybe: ['yes'] }));
1439-
} catch (error) {
1440-
actual = error.message;
1441-
}
1442-
assert(!!actual, 'No error was thrown.');
1443-
assert(actual === expected, actual);
1444-
container.remove();
1445-
});
1446-
1447-
it('throws for non-array value', () => {
1448-
const getTemplate = ({ array }) => {
1449-
return html`
1450-
<div id="target">
1451-
${repeat(array, () => {}, () => html``)}
1452-
</div>
1453-
`;
1454-
};
1455-
const container = document.createElement('div');
1456-
document.body.append(container);
1457-
let error;
1458-
try {
1459-
render(container, getTemplate({ array: 5 }));
1460-
} catch (e) {
1461-
error = e;
1462-
}
1463-
assert(error?.message === 'Unexpected repeat items "5" provided, expected an array.', error?.message);
1464-
container.remove();
1465-
});
1466-
1467-
it('throws for non-template callback value', () => {
1468-
const getTemplate = ({ array }) => {
1469-
return html`
1470-
<div id="target">
1471-
${repeat(array, item => item.id, item => item.value ? html`<div>${item.value}</div>` : null)}
1472-
</div>
1473-
`;
1474-
};
1475-
const container = document.createElement('div');
1476-
document.body.append(container);
1477-
let error;
1478-
try {
1479-
render(container, getTemplate({ array: [{ id: 'foo', value: null }] }));
1480-
} catch (e) {
1481-
error = e;
1482-
}
1483-
assert(error?.message === 'Unexpected non-template value found in map entry at 0 "null".', error?.message);
1484-
container.remove();
1485-
});
1486-
1487-
it('throws for non-template callback value (on re-render)', () => {
1488-
const getTemplate = ({ array }) => {
1489-
return html`
1490-
<div id="target">
1491-
${repeat(array, item => item.id, item => item.value ? html`<div>${item.value}</div>` : null)}
1492-
</div>
1493-
`;
1494-
};
1495-
const container = document.createElement('div');
1496-
document.body.append(container);
1497-
render(container, getTemplate({ array: [{ id: 'foo', value: 'oh hai' }] }));
1498-
let error;
1499-
try {
1500-
render(container, getTemplate({ array: [{ id: 'foo', value: null }] }));
1501-
} catch (e) {
1502-
error = e;
1503-
}
1504-
assert(error?.message === 'Unexpected non-template value found in map entry at 0 "null".', error?.message);
1505-
container.remove();
1506-
});
1507-
});
1508-
});
1509-
1510-
it('confirm that deprecation warnings are still necessary', () => {
1511-
for (const message of localMessages) {
1512-
assert(seen.has(message), `Unused deprecation warning: ${message}`);
1513-
}
1514-
});

ts/x-template.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
export const render: any;
22
export const html: any;
3-
export const ifDefined: any;
4-
export const repeat: any;
53
//# sourceMappingURL=x-template.d.ts.map

ts/x-template.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)