Skip to content

Commit 8189f09

Browse files
[lit-html] Don't remove attributes with noChange on first render (#1922)
1 parent 0b4d6ed commit 8189f09

File tree

4 files changed

+67
-5
lines changed

4 files changed

+67
-5
lines changed

.changeset/curvy-suns-confess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'lit-html': patch
3+
---
4+
5+
Binding `noChange` into an interpolated attribute expression now no longer removes the attribute on first render - instead it acts like an empty string. This is mostly noticable when using `until()` without a fallback in interpolated attributes.

packages/lit-html/src/lit-html.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1408,7 +1408,7 @@ class AttributePart implements Disconnectable {
14081408
this._$parent = parent;
14091409
this.options = options;
14101410
if (strings.length > 2 || strings[0] !== '' || strings[1] !== '') {
1411-
this._$committedValue = new Array(strings.length - 1).fill(nothing);
1411+
this._$committedValue = new Array(strings.length - 1).fill(new String());
14121412
this.strings = strings;
14131413
} else {
14141414
this._$committedValue = nothing;

packages/lit-html/src/test/directives/until_test.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ suite('until directive', () => {
127127
test('renders a Promise to an interpolated attribute', async () => {
128128
const promise = Promise.resolve('foo');
129129
render(html`<div test="value:${until(promise)}"></div>`, container);
130+
assert.equal(
131+
stripExpressionMarkers(container.innerHTML),
132+
'<div test="value:"></div>'
133+
);
134+
await promise;
135+
assert.equal(
136+
stripExpressionMarkers(container.innerHTML),
137+
'<div test="value:foo"></div>'
138+
);
139+
});
140+
141+
test('renders a nothing fallback to an interpolated attribute', async () => {
142+
const promise = Promise.resolve('foo');
143+
render(
144+
html`<div test="value:${until(promise, nothing)}"></div>`,
145+
container
146+
);
130147
assert.equal(stripExpressionMarkers(container.innerHTML), '<div></div>');
131148
await promise;
132149
assert.equal(
@@ -435,7 +452,10 @@ suite('until directive', () => {
435452
)}"></div>`,
436453
container
437454
);
438-
assert.equal(stripExpressionMarkers(container.innerHTML), '<div></div>');
455+
assert.equal(
456+
stripExpressionMarkers(container.innerHTML),
457+
'<div data-attr="other "></div>'
458+
);
439459

440460
await laterTask();
441461
assert.equal(
@@ -536,7 +556,10 @@ suite('until directive', () => {
536556
)}"></div>`,
537557
container
538558
);
539-
assert.equal(stripExpressionMarkers(container.innerHTML), '<div></div>');
559+
assert.equal(
560+
stripExpressionMarkers(container.innerHTML),
561+
'<div data-attr="other "></div>'
562+
);
540563

541564
await laterTask();
542565
assert.equal(

packages/lit-html/src/test/lit-html_test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ suite('lit-html', () => {
10041004
assertContent('<div>baz</div><div foo="bar"></div>');
10051005
});
10061006

1007-
test('renders undefined in attributes', () => {
1007+
test('renders undefined in interpolated attributes', () => {
10081008
render(html`<div attribute="it's ${undefined}"></div>`, container);
10091009
assert.equal(
10101010
stripExpressionComments(container.innerHTML),
@@ -1022,6 +1022,32 @@ suite('lit-html', () => {
10221022
assertContent('<div attribute=""></div>');
10231023
});
10241024

1025+
test('renders empty string in attributes', () => {
1026+
render(html`<div attribute="${''}"></div>`, container);
1027+
assertContent('<div attribute=""></div>');
1028+
});
1029+
1030+
test('renders empty string in interpolated attributes', () => {
1031+
render(html`<div attribute="foo${''}"></div>`, container);
1032+
assertContent('<div attribute="foo"></div>');
1033+
});
1034+
1035+
test('initial render of noChange in fully-controlled attribute', () => {
1036+
render(html`<div attribute="${noChange as any}"></div>`, container);
1037+
assertContent('<div></div>');
1038+
});
1039+
1040+
test('renders noChange in attributes, preserves outside attribute value', () => {
1041+
const go = (v: any) =>
1042+
render(html`<div attribute="${v}"></div>`, container);
1043+
go(noChange);
1044+
assertContent('<div></div>');
1045+
const div = container.querySelector('div');
1046+
div?.setAttribute('attribute', 'A');
1047+
go(noChange);
1048+
assertContent('<div attribute="A"></div>');
1049+
});
1050+
10251051
test('nothing sentinel removes an attribute', () => {
10261052
const go = (v: any) => html`<div a=${v}></div>`;
10271053
render(go(nothing), container);
@@ -1063,9 +1089,17 @@ suite('lit-html', () => {
10631089
assert.isEmpty(observer.takeRecords());
10641090
});
10651091

1066-
test('noChange works on one of multiple expressions', () => {
1092+
test('noChange renders as empty string when used in interpolated attributes', () => {
10671093
const go = (a: any, b: any) =>
10681094
render(html`<div foo="${a}:${b}"></div>`, container);
1095+
1096+
go('A', noChange);
1097+
assert.equal(
1098+
stripExpressionComments(container.innerHTML),
1099+
'<div foo="A:"></div>',
1100+
'A'
1101+
);
1102+
10691103
go('A', 'B');
10701104
assert.equal(
10711105
stripExpressionComments(container.innerHTML),

0 commit comments

Comments
 (0)