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

A way for attributes to contain expressions without triggering browser errors or false loading #833

Merged
merged 23 commits into from
Oct 4, 2023

Conversation

DmitrySharabin
Copy link
Member

@DmitrySharabin DmitrySharabin commented Apr 18, 2022

Closes #475.

I believe this feature is a nice to have in the upcoming release. I tested it on several examples, including SVG Path Builder. It looks like it's working. :)

I liked the idea of using the mv-attr-* family of attributes for the feature, so I went that way.

Since I'm not 100% sure that I do everything correctly, let's consider this PR as the first step. :)

@DmitrySharabin DmitrySharabin requested a review from LeaVerou April 18, 2022 20:25
@DmitrySharabin
Copy link
Member Author

DmitrySharabin commented Apr 19, 2022

Here is a couple of examples: https://codepen.io/dmitrysharabin/pen/gOoqOzj

@DmitrySharabin DmitrySharabin marked this pull request as draft April 19, 2022 11:27
@DmitrySharabin
Copy link
Member Author

DmitrySharabin commented Apr 19, 2022

There is an issue I don't know how to address yet: I can't preserve an attribute case. E.g., mv-attr-veiwBox is converted to mv-attr-viewbox. And it doesn't let us use the mv-attr-* family of attributes with a bunch of SVG attributes.

I tried to add different attributes with letters in different cases to elements inside and outside a Mavo app, and it looks like browsers convert attributes to lower case automatically? Am I right?

@LeaVerou Are you aware of any way to bypass it?

@DmitrySharabin
Copy link
Member Author

I tried to add different attributes with letters in different cases to elements inside and outside a Mavo app, and it looks like browsers convert attributes to lower case automatically? Am I right?

It looks like I am. 😭
https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#lower_casing

@karger
Copy link
Collaborator

karger commented Apr 19, 2022 via email

@karger
Copy link
Collaborator

karger commented Apr 19, 2022 via email

@LeaVerou
Copy link
Member

Oh I'm glad you're working on this.
Is the casing issue cosmetic or does it actually cause loss of functionality?

Due to the way Mavo resolves expressions, substituting gradually, I imagine (without having seen the code) that this solution may still produce errors when the expressions depend on other expressions, e.g.:

<input type=number value="[foo]">
<span property="foo" value="[bar]"></span>
<span property="bar" value="3"></span>

@DmitrySharabin
Copy link
Member Author

Is the casing issue cosmetic or does it actually cause loss of functionality?

In the SVG Path Builder we won't be able to get rid of the error caused by this code: viewBox="0 0 [width] [height]".

Due to the way Mavo resolves expressions, substituting gradually, I imagine (without having seen the code) that this solution may still produce errors when the expressions depend on other expressions...

Do you mean trigger browser errors? The following code works without errors (I updated the pen). :) But I'm almost 100% sure I could miss some use cases.

<input type=number mv-attr-value="[foo]" />
<span property="foo" mv-value="bar"></span>
<span property="bar">3</span>

@DmitrySharabin
Copy link
Member Author

actually, here is another possible approach: apparently there is a specified list of tag adjustments that you could perform automatically if the node with the attributes is an svg node which I suspect can be checked somehow.

That's interesting. Thanks, David. 🙏🏻

I was thinking of the approach you suggested and wondering: do we really need this at the core? If the casing issue becomes a problem for authors, we can add a plugin that will make the mv-attr-* family of attributes case-sensitive. What do you think?

@LeaVerou
Copy link
Member

Is the casing issue cosmetic or does it actually cause loss of functionality?

In the SVG Path Builder we won't be able to get rid of the error caused by this code: viewBox="0 0 [width] [height]".

Why not? Can't we specify mv-attr-viewBox and then your code would specify viewbox (lowercase), which I think would still work?

@DmitrySharabin
Copy link
Member Author

Why not? Can't we specify mv-attr-viewBox and then your code would specify viewbox (lowercase), which I think would still work?

Ah, yes. You are absolutely right—lowercase attributes are not even an issue. They work perfectly fine. Stupid me. 🤦‍♂️

Now I need to figure out why viewBox, for example, is not live if it's set via mv-attr-viewbox. That's why I thought attributes case is an issue.

Thank you. I'll continue my investigation then.

…ithout triggering browser errors or false loading
@DmitrySharabin DmitrySharabin force-pushed the fix-for-475 branch 2 times, most recently from 85c212b to d378d55 Compare April 19, 2022 21:33
…at are part of the value, not only the value itself
@DmitrySharabin
Copy link
Member Author

DmitrySharabin commented Apr 20, 2022

Thanks to David, we know that browsers adjust SVG attributes that are not all lowercase. That means that when there is an expression inside the value of an attribute (e.g., inside viewBox), Mavo gets the case-sensitive version of the attribute name to work with (e.g., viewBox). As a result, viewBox="0 0 [width] [height]" works perfectly fine (the view box adjusts according to the values of width and height).

The mv-attr-* attribute, on the other hand, is not the standard SVG attribute. So, for the mv-attr-viewBox Mavo will get the mv-attr-viewbox (all lowercase) to work with. And even though SVG understands viewbox="0 0 [width] [height]" (when added directly to <svg>), if this attribute is added through mv-attr-viewbox="0 0 [width] [height]", the expressions don't have any effect on the SVG. Though, the viewbox attribute is being updated via expressions. The same thing happens with preserveAspectRatio.

We can conclude that the casing issue causes loss of functionality. So I decided to follow David's suggestion and adjust those kinds of attributes manually. And it worked. Yay!

I updated the demo so that it showcases different use cases.
UPDATE: Refactored SVG Path builder—no console errors! 🥳

I believe this PR is ready for review. 🤞🏻

@DmitrySharabin DmitrySharabin marked this pull request as ready for review April 20, 2022 10:10
@LeaVerou
Copy link
Member

Are you using the correct namespace for these attributes? I wonder if that's why viewbox doesn't work.

@DmitrySharabin
Copy link
Member Author

Are you using the correct namespace for these attributes? I wonder if that's why viewbox doesn't work.

Hmm. That's a good question. I don't know. I definitely need to figure it out.

@DmitrySharabin
Copy link
Member Author

DmitrySharabin commented Apr 21, 2022

Here are some results of my research concerning SVG, its elements, attributes, and namespaces:

  1. Regardless of whether we use viewBox or viewbox, the browser adjusts it to viewBox (and adds the corresponding attribute if needed).

Suppose we have (mind the attribute case):

<svg viewbox="0 0 100 100">
  ...
</svg>

In DevTools, we see (mind the attribute case):

Screen Shot 2022-04-21 at 19 09 42

After inspecting the <svg> element and invoking console.dir($0) in the console, we get:

Screen Shot 2022-04-21 at 19 05 19

  1. Attributes of SVG elements don't have namespaceURI (even though the corresponding elements do). Except href, IIRC. See the previous screenshot.

  2. Regardless the point 2 (it didn't stop me 😂), I searched through the codebase and found the method where we set attributes: Primitive.setValue(). There, I replaced this line

element.setAttribute(o.attribute, value);

with this one:

if (element.namespaceURI === "http://www.w3.org/2000/svg") {
  element.setAttributeNS(element.namespaceURI, o.attribute, value);
}
else {
  element.setAttribute(o.attribute, value);
}

Then I inspected <svg> added by the following code (mind the fallback for viewBox and its case):

<svg
  viewbox="-50 -50 50 50"
  mv-attr-viewBox="[list(0, 0, 100, 100)]">
  ...
</svg>

In DevTools we have

Screen Shot 2022-04-21 at 21 19 01

and

Screen Shot 2022-04-21 at 21 22 27

As we can see, the browser adjusted the fallback attribute (as expected), and the one added by Mavo has the namespaceURI property and is evaluated correctly (its value is the result of an expression evaluation), though it doesn't have any effect. It's simply ignored by the browser. Moreover, every other mv-attr-* attribute stopped working (even though without namespace they worked fine).

To conclude, namespaceURI doesn't have any positive effect in our case.

So, unfortunately I couldn't find the way to make mv-attr-* work with case-sensitive SVG attributes without manually tweaking them (as I do it in this PR). 😔 However, I can't deny that there might be something I still don't get. 🤷🏻‍♂️

@LeaVerou
Copy link
Member

LeaVerou commented Apr 21, 2022

I believe namespaceURI is null when it can be determined via the parent, ancestor etc namespace. It's not null on href because href has the XLink namespace implicitly (in fact it used to be written xlink:href).

I wonder if something like this would work: https://codepen.io/leaverou/pen/ZEvPPaP?editors=1111
The problem is, you don't want to change casing if e.g. someone sets a viewbox attribute on an HTML element, so you can't just match on attribute names. Context matters.

@DmitrySharabin
Copy link
Member Author

I wonder if something like this would work: https://codepen.io/leaverou/pen/ZEvPPaP?editors=1111

Whoa! That's cool!!! Thanks for the new knowledge.

Re:context. Got it. Totally agree. I will take that into account.

@DmitrySharabin
Copy link
Member Author

Done!

@DmitrySharabin
Copy link
Member Author

One complication is that not all attributes will work on the root element, some of them may need to be on specific elements. You should try and see if it works for e.g. refX and that sort of thing.

If I get your comment correctly, now it should work (after some fixes I made to the code): https://codepen.io/dmitrysharabin/pen/mdpgBvy

@DmitrySharabin
Copy link
Member Author

@LeaVerou, I would also be grateful if you give this PR another look. I believe the feature it adds to Mavo might be very beneficial. We might make this feature experimental.

@adamjanicki2
Copy link
Collaborator

adamjanicki2 commented Sep 25, 2023

I think as long as it's deterministic, there are many right choices. The main thing we should avoid is it being non-deterministic, i.e. sometimes it overrides it and sometimes it doesn't depending on race conditions.

Definitely on the same page here, a non-deterministic scenario would be very much not ideal 😂

I agree mv-attr-foo always having precedence over foo makes sense. I could also be persuaded that the order of attributes can specify precedence.

I think overall I like having mv-attr-attrname taking precedence or attrname, but also there is some precedence with ordering in other frameworks, where typically the last attr specified takes precedence (for example in react if you have something like <Element {...props} className="..." />), so I could also be persuaded for an ordering, but since Mavo has this specific feature for defining attributes, the goal should be encouraging users to use them over the default ones, which is why I like Mavo attrs taking precedence

Regardless of what we decide, we definitely will need to update the documentation to be very clear and explicit about how this works

Comment on lines 231 to 245
let attributes = $$(node.attributes);

// Iterate only over mv-attr-foo with expressions
for (let attribute of attributes.filter(attribute => attribute.name.startsWith("mv-attr-") && syntax.test(attribute.value))) {
let name = attribute.name.replace("mv-attr-", "");
let ignorable = attributes.find(attribute => attribute.name === name);

if (ignorable) {
// Ignore expressions in foo, if any, since mv-attr-foo has priority
ignore ??= new Set();
ignore.add(ignorable.name);
}
}

attributes.forEach(attribute => {
Copy link
Member Author

Choose a reason for hiding this comment

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

@LeaVerou, thank you so much for your hint—expressions.js is indeed the appropriate place for this logic. Sorry for requesting your review one more time. 🙈

Copy link
Member

@LeaVerou LeaVerou Oct 2, 2023

Choose a reason for hiding this comment

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

I think this may be complicating things a bit. I just pushed a commit that tries to simplify the logic, but I haven't tested it, so you'd need to do that.

Also, we don't need to apply syntax.test() here, the values of mv-attr-*, like directives, are always expressions. If you haven't designed it like that, you should.

Copy link
Member Author

Choose a reason for hiding this comment

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

I re-designed the mv-attr-* feature to work as mv-value. Because of the wrong design decision, I wrote way too complicated logic. I knew that mv-attr-foo doesn't make sense if it's not an expression, but I chose the wrong path to implement it. Thank you for pointing this out. 🙏🏻

this.attribute = Mavo.getProperAttributeCase(this.element, this.attribute);
}

this.fallback = this.fallback || Mavo.Primitive.getValue(this.element, {attribute: this.attribute});
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
this.fallback = this.fallback || Mavo.Primitive.getValue(this.element, {attribute: this.attribute});
this.fallback ||= Mavo.Primitive.getValue(this.element, {attribute: this.attribute});

(also do we need || or ?? here?)

Copy link
Member Author

Choose a reason for hiding this comment

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

I assume that some falsy values (e.g., 0) might be perfectly valid fallbacks, and we don't want to replace them with any other value. So, I'm leaning towards ??=.

Copy link
Member

@LeaVerou LeaVerou left a comment

Choose a reason for hiding this comment

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

Looks good, left some minor comments

@@ -183,11 +183,11 @@ var _ = Mavo.Expressions = $.Class({
},

extract: function(node, attribute, path, syntax = Mavo.Expression.Syntax.default) {
if (attribute && _.skip.indexOf(attribute.name) > -1) {
if (_.skip.includes(attribute?.name)) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove this entirely now that _.skip is taken into account in traverse()? As far as our own codebase is concerned, that seems ok, but not sure if any plugins call extract() and depend on this behavior

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. I searched for extract( in our repo with plugins and found no mentions. Neither could I find this in my repos. I'm unsure if we have any other plugins we might break. We can try to commit to this change and see what might happen. 🤓

Copy link
Member Author

@DmitrySharabin DmitrySharabin Oct 3, 2023

Choose a reason for hiding this comment

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

I'm ready to remove it as soon as we decide to do that. I commented this block out, and everything still works.

Copy link
Member

Choose a reason for hiding this comment

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

Let's do it!

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

Now, I'm ready to request your review one more time. Thanks a ton for your patience and excellent ideas! 🙏🏻

this.attribute = this.attribute.replace("mv-attr-", "");

if ([SVG_NAMESPACE_URI, MATHML_NAMESPACE_URI].includes(this.element.namespaceURI)) {
this.attribute = Mavo.getProperAttributeCase(this.element, this.attribute);
Copy link
Member

@LeaVerou LeaVerou Oct 4, 2023

Choose a reason for hiding this comment

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

Just dawned on me: do we also need to do this in XHTML documents (with an actual XML MIME type)? If so, it would require a different check (not a blocker, we can fix later).

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably we should. Our users will push us to do this in any case. 😅


this.fallback ??= Mavo.Primitive.getValue(this.element, {attribute: this.attribute});
let expression = this.element.getAttribute(this.originalAttribute);
this.element.removeAttribute(this.originalAttribute);
Copy link
Member

Choose a reason for hiding this comment

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

There seems to be a lot of repetition between this and the preceding block implementing mv-value. I'll push a commit to address it.

Copy link
Member

Choose a reason for hiding this comment

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

Done, please check?

Copy link
Member Author

Choose a reason for hiding this comment

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

Whoa! Cool! It's working with this beautiful piece of code. 🥳 Thank you!!

Copy link
Member

@LeaVerou LeaVerou left a comment

Choose a reason for hiding this comment

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

Looks like this is done then?

@DmitrySharabin
Copy link
Member Author

I think so! 🤞

@DmitrySharabin DmitrySharabin merged commit 555fae2 into master Oct 4, 2023
@DmitrySharabin DmitrySharabin deleted the fix-for-475 branch October 4, 2023 19:01
@DmitrySharabin
Copy link
Member Author

@LeaVerou, thanks a ton for your help! We made this awesome feature happen. Yay! 🥳

@karger
Copy link
Collaborator

karger commented Oct 6, 2023

I think as long as it's deterministic, there are many right choices. The main thing we should avoid is it being non-deterministic, i.e. sometimes it overrides it and sometimes it doesn't depending on race conditions.

I agree mv-attr-foo always having precedence over foo makes sense. I could also be persuaded that the order of attributes can specify precedence.

Thoughts, @karger @adamjanicki2 ?

I think the natural use case for foo= mv-attr-foo= is when you want to give the browser something to work with until the expression in mv-attr-foo is evaluated, but that once that happens mv-attr-foo should dominate.

@DmitrySharabin
Copy link
Member Author

I think the natural use case for foo= mv-attr-foo= is when you want to give the browser something to work with until the expression in mv-attr-foo is evaluated, but that once that happens mv-attr-foo should dominate.

This is exactly how it’s designed and implemented. I’m glad we are all on the same page. ☺️

@karger
Copy link
Collaborator

karger commented Nov 5, 2023

I think the natural use case for foo= mv-attr-foo= is when you want to give the browser something to work with until the expression in mv-attr-foo is evaluated, but that once that happens mv-attr-foo should dominate.

This is exactly how it’s designed and implemented. I’m glad we are all on the same page. ☺️

Having considered further, I can see an argument for order of attributes, because that's how html works, right? So once the expression is evaluated. we could apply the standard order of attributes rules to decide if it overrides the foo attribute.

@DmitrySharabin
Copy link
Member Author

@karger, do you mind filing a separate issue so we can discuss it there, not in the merged PR? There is always room for improvements we don't want to skip. :)

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.

Provide a way for attributes to contain expressions without triggering browser errors or false loading
4 participants