-
Notifications
You must be signed in to change notification settings - Fork 955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor AttributePart to only have a single value #400
Conversation
src/core.ts
Outdated
} | ||
throw new Error(`Unknown part type ${templatePart.type}`); | ||
throw [new Error(`Unknown part type ${templatePart.type}`)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I'll add some more comments, but now there's not a 1-1 correspondence between "template parts" and Part
s. Maybe there's some further refinement there, but we need something with knowledge of a whole attribute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It just seems weird to me to be throwing an Array<Error>
, especially because there's no special handling of this array anywhere in lit-html itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, I didn't even see that it was an array of Error, I read this as a line returning multiple parts! Thanks!
(part as MultiPart).setValue(values, valueIndex); | ||
valueIndex += part.size; | ||
if (part !== undefined) { | ||
part.commit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't the current setup lead to multiple commits for some attributes?
e.g.
const template = (one, two) => html`<my-element some-attr="${one} ${two}"></my-element>`;
render(template('lorem', 'ipsum'), container);
// later on
render(template('dolor', 'sit'), container);
Won't that change the some-attribute
to "dolor sit"
twice? This loop commits every part, and both AttributePart
s in the attribute commit their AttributeCommitter
because they're both dirty.
A possible solution would be to mark all parts of an AttributeCommitter
as not dirty when running AttributeCommitter#commit()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a mistake, I was switching patterns and forgot to make committing only run once. I'll fix asap :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed and added a test
@sorvell this is ready for review now. I like the shape of the API now, with the small exception of PartCallback returning an array of Parts. I didn't yet explore returning multiple TemplateParts for attributes. I can look into that or not... I don't think there will be that many PartCallback implementations our there. core.ts gets a tiny bit smaller with this factoring, by about ~40 bytes. |
e715f3c
to
ad7b5ca
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of nits and questions ...
src/core.ts
Outdated
if (part !== undefined) { | ||
const v = part._value; | ||
if (v != null && | ||
(Array.isArray(v) || typeof v !== 'string' && v[Symbol.iterator])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TODO
went away and it's not obvious why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if one of the values is a Promise or a TemplateResult or a Node? Is it worth handling any of those weird cases more gracefully?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this TODO was mostly incorrect, and fully incorrect now that this code is in AttributeCommitter. The values here are after AttributePart has already handled them, passing them to getValue. Directive handling is the biggest thing we get out of calling getValue, and arrays of directive don't make sense, since there's no individual part to pass to the directive. TemplateResults don't make sense for attribute parts - using them here would be essentially toString'ing a DOM tree into an attribute.
let i = 0; | ||
for (const part of this._parts) { | ||
if (part !== undefined) { | ||
part.setValue(values[i]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since AttributePart
's actually apply to DOM during commit
, but NodePart
s do so in setValue
, this could be an observable change to, say, a custom element. Does that concern you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, right now all attribute changes will apply before children changes. That shouldn't affect any well behaved elements, and won't affect anything that uses batching. I guess we can change this so it goes in DOM order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it looks a bit tricky to defer the work in _setValue()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still concerns me a bit. For example, events happen in commit
but nodes are done in setValue
so an event listener installed on a parent wouldn't see an event fired by a child custom element at connect time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, this has been addressed since NodePart now applies changes to DOM in commit
.
node as Element, | ||
templatePart.name!.slice(1), | ||
templatePart.strings!); | ||
return comitter.parts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems somewhat weird that you make AttributePart
s for properties, but I'm not sure this is worth addressing, wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually do think we should add a trivial PropertyPart subclass of AttributePart just in case there's a need for a difference in behavior in the future and we want that change to be less likely to break anyone.
I'll add this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done btw
@sorvell added PropertyPart. PTAL |
let i = 0; | ||
for (const part of this._parts) { | ||
if (part !== undefined) { | ||
part.setValue(values[i]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still concerns me a bit. For example, events happen in commit
but nodes are done in setValue
so an event listener installed on a parent wouldn't see an event fired by a child custom element at connect time.
Good example. I'll write a test for this case. |
TODO: lit-extended tests, fix AttributeCommitter to write only once per attribute.
src/lit-html.ts
Outdated
} else { | ||
// Interpolation, so interpolate | ||
value = this._interpolate(values, startIndex); | ||
this._value = !!value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line should be after the dirty check.
39af054
to
d01c7cd
Compare
@sorvell PTAL |
45c435c
to
438b052
Compare
node as Element, | ||
templatePart.name!.slice(1), | ||
templatePart.strings!); | ||
return comitter.parts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done btw
src/lit-html.ts
Outdated
|
||
setValue(value: any): void { | ||
this._pendingValue = value; | ||
// value = !!value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also should add documentation about the changes to directives, and are there any examples to update?
src/core.ts
Outdated
} | ||
return value === null ? undefined : value; | ||
}; | ||
// export const getValue = (part: Part, value: any) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove cruft
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/core.ts
Outdated
@@ -462,126 +462,153 @@ export const noChange = {}; | |||
*/ | |||
export {noChange as directiveValue}; | |||
|
|||
export const _isPrimitiveValue = (value: any) => value === null || | |||
!(typeof value === 'object' || typeof value === 'function'); | |||
export const _isPrimitiveValue = (value: any) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exporting a _
prefixed function is weird since that implies private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to isPrimitive
src/core.ts
Outdated
value = value === null ? undefined : value; | ||
if (value !== noChange || !_isPrimitiveValue(value) || value !== this._value) { | ||
this._value = value; | ||
if (!isDirective(value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment explaining why this is important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/core.ts
Outdated
} | ||
|
||
setValue(value: any): void { | ||
value = value === null ? undefined : value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why reset null
to undefined
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was inlined from getValue()
, but I think it's only necessary on NodePart. Testing without it.
src/core.ts
Outdated
} else { | ||
value = this._interpolate(values, startIndex); | ||
commit() { | ||
// console.log('commit', this._value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove comment
src/core.ts
Outdated
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
const value = this._pendingValue === null ? undefined : this._pendingValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, why reset null
to undefined
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
textContent = null
would render "null", so this conversion was done in getValue()
previous. Moved into _setText()
src/core.ts
Outdated
if (value === this._previousValue) { | ||
return; | ||
if (value !== this._value) { | ||
this._setText(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these functions be called commit*
since they occur in the commit phase? This seems easier to understand since setValue
is a different phase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
let i = 0; | ||
for (const part of this._parts) { | ||
if (part !== undefined) { | ||
part.setValue(values[i]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, this has been addressed since NodePart now applies changes to DOM in commit
.
src/core.ts
Outdated
|
||
export class AttributePart implements Part { | ||
committer: AttributeCommitter; | ||
_value: any = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems wrong that this is _value
since directives can and do access it. (Same for NodePart).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to value
src/lit-html.ts
Outdated
|
||
setValue(value: any): void { | ||
this._pendingValue = value; | ||
// value = !!value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove cruft
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sorvell PTAL
src/core.ts
Outdated
|
||
export class AttributePart implements Part { | ||
committer: AttributeCommitter; | ||
_value: any = undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to value
src/core.ts
Outdated
} | ||
|
||
setValue(value: any): void { | ||
value = value === null ? undefined : value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was inlined from getValue()
, but I think it's only necessary on NodePart. Testing without it.
src/core.ts
Outdated
value = value === null ? undefined : value; | ||
if (value !== noChange || !_isPrimitiveValue(value) || value !== this._value) { | ||
this._value = value; | ||
if (!isDirective(value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/core.ts
Outdated
value = this._interpolate(values, startIndex); | ||
commit() { | ||
// console.log('commit', this._value); | ||
while (isDirective(this._value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/core.ts
Outdated
this._pendingValue = noChange; | ||
directive(this); | ||
} | ||
const value = this._pendingValue === null ? undefined : this._pendingValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
textContent = null
would render "null", so this conversion was done in getValue()
previous. Moved into _setText()
src/core.ts
Outdated
if (value === this._previousValue) { | ||
return; | ||
if (value !== this._value) { | ||
this._setText(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
src/lit-html.ts
Outdated
|
||
setValue(value: any): void { | ||
this._pendingValue = value; | ||
// value = !!value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
|
||
commit() { | ||
while (isDirective(this._pendingValue)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly a little, maybe on the base class. I'd like to do another refactoring attempt later as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This PR removes the SinglePart/MultiPart distinction. AttributeParts now only have a single value, but are associated with an AttributeCommitter that commits multiple values to an attribute.
This PR is based on #398.
The attribute handling code is a bit simpler in this factoring, though the "fast" path for attribute with a single value isn't implemented here, but it is implemented for PropertyCommitter. Not sure if this matters much.