Skip to content
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

Merged
merged 26 commits into from
Aug 17, 2018
Merged

Conversation

justinfagnani
Copy link
Collaborator

@justinfagnani justinfagnani commented Jul 14, 2018

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.

src/core.ts Outdated
}
throw new Error(`Unknown part type ${templatePart.type}`);
throw [new Error(`Unknown part type ${templatePart.type}`)];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional?

Copy link
Collaborator Author

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 Parts. Maybe there's some further refinement there, but we need something with knowledge of a whole attribute.

Copy link
Contributor

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.

Copy link
Collaborator Author

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();
Copy link
Contributor

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 AttributeParts 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()

Copy link
Collaborator Author

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 :)

Copy link
Collaborator Author

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

@justinfagnani
Copy link
Collaborator Author

@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.

Copy link
Member

@sorvell sorvell left a 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])) {
Copy link
Member

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.

Copy link
Member

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?

Copy link
Collaborator Author

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]);
Copy link
Member

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 NodeParts do so in setValue, this could be an observable change to, say, a custom element. Does that concern you?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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()

Copy link
Member

@sorvell sorvell Jul 28, 2018

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.

Copy link
Member

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;
Copy link
Member

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 AttributeParts for properties, but I'm not sure this is worth addressing, wdyt?

Copy link
Collaborator Author

@justinfagnani justinfagnani Jul 27, 2018

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done btw

@justinfagnani
Copy link
Collaborator Author

@sorvell added PropertyPart. PTAL

let i = 0;
for (const part of this._parts) {
if (part !== undefined) {
part.setValue(values[i]);
Copy link
Member

@sorvell sorvell Jul 28, 2018

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.

@justinfagnani
Copy link
Collaborator Author

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.

src/lit-html.ts Outdated
} else {
// Interpolation, so interpolate
value = this._interpolate(values, startIndex);
this._value = !!value;
Copy link
Contributor

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.

@justinfagnani
Copy link
Collaborator Author

@sorvell PTAL

node as Element,
templatePart.name!.slice(1),
templatePart.strings!);
return comitter.parts;
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove

Copy link
Member

@sorvell sorvell left a 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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove cruft

Copy link
Collaborator Author

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) =>
Copy link
Member

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.

Copy link
Collaborator Author

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)) {
Copy link
Member

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.

Copy link
Collaborator Author

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;
Copy link
Member

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?

Copy link
Collaborator Author

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);
Copy link
Member

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;
Copy link
Member

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?

Copy link
Collaborator Author

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);
Copy link
Member

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.

Copy link
Collaborator Author

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]);
Copy link
Member

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;
Copy link
Member

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).

Copy link
Collaborator Author

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove cruft

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Collaborator Author

@justinfagnani justinfagnani left a 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;
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

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)) {
Copy link
Collaborator Author

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)) {
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

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)) {
Copy link
Collaborator Author

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.

Copy link
Member

@sorvell sorvell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants