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

[lit-html] add custom equality support to live #4763

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

cristinecula
Copy link

Added the ability to provide a custom equality function as the second argument to live, to be able to handle cases where a type conversion happens internally.

Added the ability to provide a custom equality function as the second argument to live, to be able
to handle cases where a type conversion happens internally.
Copy link

changeset-bot bot commented Sep 13, 2024

🦋 Changeset detected

Latest commit: d598cdb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
lit-html Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

google-cla bot commented Sep 13, 2024

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@justinfagnani
Copy link
Collaborator

@cristinecula thanks for the PR

Was there an issue yet for this design? At a glance, this looks like a nice change, but we do ask that we have an issue or RFC to discuss additions before PRs: https://github.com/lit/lit/blob/main/CONTRIBUTING.md#pull-requests

I'll take a look at the change soon!

@cristinecula
Copy link
Author

Hi, @justinfagnani!

It's an issue I ran into during my work. Sorry, I should have reported it first. The gist of it is this:

I have a number input and I store it's value as a number. I use live to bind the value back to the value property. Because there is a type mismatch, live will update the property all the time. This causes UX issues when users try to edit decimals.

You can test it in this reproduction.

Reproduction steps:

  1. type 1.5 in the number input
  2. attempt to change the value to 1.2, by deleting 5 with backspace and typing 2
  3. observe that the input now says 21 instead of 1.2 (on chromium, on firefox it will be 12)

My addition to live would allow me to customize the equality, such that instead of strict equality, I also do a type conversion to string, thus preventing lit from unnecessarily updating the value property.


While writing the issue description and reproduction gists, I've come to realize that I can bind to valueAsNumber, which simply works out of the box. This fixes my original issue, rendering this PR unnecessary. I guess it could still be useful in some situations. What do you think?

@cristinecula
Copy link
Author

Here is an example where having a custom live equality function makes a lot of sense:

When you want to store a number as fixed point notation.

Playground

Try writing 123 in both inputs.

@justinfagnani
Copy link
Collaborator

Thanks for the details @cristinecula. These are the things that would be great in an issue. You can even open an issue and PR at the some time so the issue can be the point of reference if there are changes in how we solve something. Like if we decide this is working fine, people might find that in a closed issue, but not a closed PR.

As for whether we still need this change... tough call. In general I like the feature, but I'm by default slightly hesitant to add more code w/o a clear use case example. Thanks for the second example. It behaves a little funny because of the rounding behavior of toFixed. If you type in 1.23456 it'll get rounded to 1.235 and from there any keystroke on 5 or greater will just increment the number by .001 :)

But I think the important takeaway focus management. live() gives us the ability to overwrite the binding value when Lit doesn't think it's necessary. This change gives us more ability to not do that, which I don't think is usually a big deal - setting a property sometimes shouldn't matter much, but in input cases it can preserve focus when you don't set a new value, which is important.

I think it might be better to actually have more customizable focus management for inputs - like to restore the cursor to a specific index, but that seems more than what live() is responsible for.

So I think this might be useful, and it's not much code and not in core. I'll get to reviewing it now 👍

Comment on lines 67 to 71
const element = part.element as Element;
const name = part.name;
const _equal = equal ?? defaults[part.type];

if (_equal(value, getValue[part.type](element, name))) return noChange;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can inline the getValue function to avoid some property lookups:

Suggested change
const element = part.element as Element;
const name = part.name;
const _equal = equal ?? defaults[part.type];
if (_equal(value, getValue[part.type](element, name))) return noChange;
const {type, name, element} = part;
const currentValue =
type === PartType.PROPERTY
? element[name as keyof typeof element]
: type === PartType.ATTRIBUTE
? element.getAttribute(name)
: element.hasAttribute(name);
if (equal(value, currentValue)) {
return noChange;
}

Copy link
Author

Choose a reason for hiding this comment

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

Updated.

part: AttributePart & {
readonly type: ValidPartType;
},
[value, equal]: DirectiveParameters<this>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Defaults can be set in the destructuring parameter:

Suggested change
[value, equal]: DirectiveParameters<this>
[value, equal = defaults[part.type]]: DirectiveParameters<this>

Copy link
Author

Choose a reason for hiding this comment

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

Updated.

@@ -88,6 +100,14 @@ class LiveDirective extends Directive {
* `live()` should not be used when the binding will cause a type conversion. If
* you use `live()` with an attribute binding, make sure that only strings are
* passed in, or the binding will update every render.
*
* To customize the equality check, you can pass in a function as the second
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the previous paragraph needs to be updated a bit to refer to that as default behavior.

Copy link
Author

Choose a reason for hiding this comment

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

Updated. I'm not sure if the wording is clear enough, but I was not sure how to make it better.

@@ -14,6 +14,24 @@ import {
} from '../directive.js';
import {isSingleExpression, setCommittedValue} from '../directive-helpers.js';

type ValidPartType = 1 | 3 | 4;
type Element = HTMLElement & {[key: string]: unknown};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we should override the global Element type, and this type could remove a lot of type-checking in the whole file. We can cast at the access site.

Copy link
Author

Choose a reason for hiding this comment

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

Resolved. Thank you for the insight. I'm still a novice at typescript.

@cristinecula
Copy link
Author

Thank you for the thorough analysis and feedback! I've updated the PR with the suggested changes.

That rounding input behavior is indeed funny. You have a keen eye to catch that!
It was actually a bug in the equality function. This updated version does not have that issue.

I'm not sure if I should go and create an issue referencing this PR or not. Please let me know and I will do it ASAP!

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.

2 participants