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

observedStyles #856

Open
chris70c opened this issue Nov 24, 2019 · 23 comments
Open

observedStyles #856

chris70c opened this issue Nov 24, 2019 · 23 comments

Comments

@chris70c
Copy link

Sometime a custom element may need something like an orientation attribute to make it
vertically or horizontally (think to a slider for example), in theory it would be better
to have that as a css style instead of an attribute but currently there is no way to
observe a change in a style (being inline or in a class) so the custom element cannot
respond to it.

Is there any plan to add something like observedStyles() similar to observedAttributes()
or any other similar method to do it?

Sorry if this not the right place to ask...

@rniwa
Copy link
Collaborator

rniwa commented Nov 24, 2019

There is no such plan. Observing changes in the computed / used style is very expensive because browser engines don’t eagerly update the style. I don’t think we can add a feature like this unless there are very compelling use cases.

@justinfagnani
Copy link
Contributor

Apologies for this copy and paste, but I'm on my phone and this would be hard to rewrite. This is a comment of mine from another discussion on style observers, where there was a concern about their performance:

I think that's a pretty huge benefit when designing custom elements. Built-in elements are able to perform work when CSS properties change, high-fidelity custom elements should be able to as well.

Take background-image and url() as an example. There's native code that fetches the image solely in response to the CSS property.

One big downside to not having observable CSS properties is that these type of style-related parameters can't be changed by changing a stylesheet, which means that some theming/customization for components has to happen at the JS level, which might not be accessible from those trying to do the theming...

I would be happy with a very limited form of style observation. Something where you have to specify the properties, they can only be custom properties, and you can only listen on shadow roots (maybe via ElementInternals?), and that changes to observed properties on the same element from within an observer are notified on the next frame - to try to ensure that any style changes as a result of observation don't lead to runaway cycles.

@rniwa
Copy link
Collaborator

rniwa commented Nov 24, 2019

I think that's a pretty huge benefit when designing custom elements. Built-in elements are able to perform work when CSS properties change, high-fidelity custom elements should be able to as well.

That's not really a thing. It's true that some builtin elements have special style magic that responds to style changes but that's more of a missing CSS features.

Anyway, what we need is a list of concrete use cases. There is no way we're gonna add a serious foot gun like this unless there are extremely compelling reasons to do so.

@chris70c
Copy link
Author

chris70c commented Nov 24, 2019

Not sure what you mean by "extremely compelling reason", in what other way can we design custom elements that responds to style changes? That by itself should be reason enough.
Like I said what if I want to design a slider with a custom css variable to let a user choose the orientation, how can I do it right now? I can see hundreds of reasons why being able to observe changes in styles would be useful, if observedStyles() is not the right way maybe something different but it seems like something that we should really be able to do. There is already the possibility to observe changes in the style attribute via mutation observer and that is also quite slow, I would think that monitor just one or two styles by adding them like we do with observedAttributes() should be faster than that.

@bathos
Copy link

bathos commented Nov 24, 2019

We’re doing this currently in some places within a RAF loop. This has taken two forms:

  1. A property which is just e.g. keyword values, where reading the computed value implies interpolation of vars but interpretation of the value is simple.
  2. A property which expects e.g. <length>, where a useful computed value needs to end up in px form. If the usage of a value of this sort is 1:n with some existing CSS properties, no observation is needed, but if the realization of the styling effect involves ES-layer calculation or secondary effects, it is.

As far as I’m aware, the former case doesn’t trigger paint or layout. The latter may: a workaround for it is to use an existing <length> property on an internal dummy element and read that, e.g. <div style="top: var(--the-actual-property, 0px);">.

For a few property reads like this, this is pretty harmless, but I don’t think it would scale acceptably for cases where numerous elements respond to custom properties.

While the usages we have had for these strategies to date have happened to track closely to element definitions, conceptually the properties don’t ‘belong’ to the element, and indeed there are cases where more than one element honors the same custom-property-whose-realized-effect-cannot-occur-in-css-alone. So I don’t think this belongs to the custom elements API, but rather to CSS APIs. The issue is closely related to registerProperty, which will simplify interpretation and help avoid hacks when available in all browsers, and it’s also closely related to various Houdini efforts. At least one of the properties in question could probably be realized through the Houdini Layout API for example.

In other words I think this is a real need for authoring custom elements, especially UI widgets, that have coherent and consistent APIs that separate styling concerns from semantic concerns, but I don’t think it really belongs within the custom elements API.

@rniwa
Copy link
Collaborator

rniwa commented Nov 25, 2019

Not sure what you mean by "extremely compelling reason", in what other way can we design custom elements that responds to style changes? That by itself should be reason enough.

That’s not a use case. That presumes the need to define the orientation of a component in CSS. A use case would depict a concrete scenario like building a calendar widget which poses a problem, not a need for a particular solution.

For example, a concrete use case for a mode of transportation goes like: I’d like to live near a coast line but want to work in downtown, which is 20km away, and I’d like my commute to be less than 30 minutes. A particular solution to this problem might be laying out more public transportation, or have the person a buy a motorized vehicle; but the use case didn’t define or allude to any particular solution.

There is already the possibility to observe changes in the style attribute via mutation observer and that is also quite slow, I would think that monitor just one or two styles by adding them like we do with observedAttributes() should be faster than that.

MutationObserver observes content attribute change to a DOM element, not its computed or used style; the letter is way more expensive to compute because we’d have to match & apply all rules that apply to the given element and its ancestors for inheritance. There is no way we can do this sync. Remembering what the obvious style of an element was is also quite expensive. We very carefully avoid having that kind of infrastructure in WebKit right now.

@bathos
Copy link

bathos commented Nov 25, 2019

There is no way we can do this sync.

Agreed that whatever form a hook for this might take, it must not be sync. Realizing a property’s effect only matters come time to draw a new frame.

@chris70c
Copy link
Author

Let's try to make it easier than, you don't need the old value of the style, a custom element should already know its own state (vertical, horizontal), can this be applied only to custom css properties, can a callback be fired whenever any style changes without telling me what has changed, we just need to know that something happened then we can check the computedStyle on whatever property we care about, something like that.

@annevk
Copy link
Collaborator

annevk commented Nov 25, 2019

@rniwa makes a good point, anyone not already familiar with how we go about adding new features please read https://whatwg.org/faq#adding-new-features.

@rniwa
Copy link
Collaborator

rniwa commented Nov 25, 2019

Let's try to make it easier than, you don't need the old value of the style, a custom element should already know its own state (vertical, horizontal), can this be applied only to custom css properties, can a callback be fired whenever any style changes without telling me what has changed, we just need to know that something happened then we can check the computedStyle on whatever property we care about, something like that.

It may surprise you but none of that makes this feature substantially easier to implement (at least in WebKit). But again, the difficulty of implementation is secondary. The primary thing we need is a list of concrete use cases. Nothing is gonna happen until we have that.

@bathos
Copy link

bathos commented Nov 25, 2019

I can provide some examples from our popup generic (used within dropdown menu, select, etc), whose layout logic occurs partly in ES. It supports a number of CSS properties, but four in particular are needed during the ES layout calc.


CSS properties used in <kn-popup> that we observe from ES. (Snippet from docs.)

--kn-popup-bleed-width

Syntax: <length> | <percentage>
Initial: 0px

This creates a sort of ‘phantom margin’: the layout process, when selecting a
placement for the popup, will treat the containing boundaries being tested as if
they were smaller by this amount.

--kn-popup-origin-gap

Syntax: conterminous | <length>
Initial: 0px

As a length value, this specifies the distance between the origin and the popup
on the vertical axis (which could be above or below).

The keyword conterminous (look, it’s just perfect okay) produces no gap, like
0px, but has additional effects:

  • Any corner of the popover which directly touches the origin box will have its
    border-radius set to zero. Keep in mind, again, that you do not always know
    in advance which corners these will be.
  • The edge of the popover which contacts the origin box will be clipped such
    that box-shadows cannot extend past it.
  • The expansion and collapse animations will scale only on the Y axis.

Note that the shadow clipping behavior may be unsuitable if your usage is one
that does not guarantee the width of the popover is not wider than the origin
element. For such cases, you may instead wish to set --kn-popup-box-shadow to
none and place a drop shadow filter on the entire element instead.

In the future, when :state is available, we may eliminate this option and
instead provide state pseudoclasses to allow the consuming elements to respond
to the various specific conditions mentioned here directly.

--kn-popup-origin-priority

Syntax: [ center | end | left | right | start ] || [ top | bottom ]
Initial: center bottom

This sets the preferred origin point from which the popover should seem to open.

Vertical values are fairly straightforward. The top value asks to prefer using
the top edge of the origin element as the origin from which the popover expands,
in this case upwards; bottom is the opposite.

Horizontal values may require more clarification. As with top, left asks to
prefer using the left edge of the origin element as the origin from which the
popup expands, but such popup is extending towards the right.

  ______
 |______|___  <--origin element
 |          | <--popover is wider than origin. it is using a left origin point.
 |__________|

The start and end values behave the same as left and right in some order
which depends on local directionality.

The center option prefers alignment with the middle of the origin element, but
it cannot ‘swap’ with anything else. With the other four horizontal values,
their opposite value is considered higher priority than falling back on
arbitrary shifting. To understand why this should be, consider if the previous
example’s left preference had been unsatisfiable:

      ______  |<--edge of screen — doesn’t fit opening from the left
     |______|_|_
     |        | |
     |________|_|
              |

If the prioritized value aligned with a side, it is taken to indicate a
preference not just for that specific side, but also for aligning with a side
period:

     SWAP     |    |     SHIFT    |
      ______  |    |      ______  |
  ___|______| |    |   __|______| |
 |          | | VS |  |          ||
 |__________| |    |  |__________||
              |    |              |

--kn-popup-width-mode

Syntax: auto | min-origin | origin
Initial: auto

The origin value expressly binds the width of the popup to the origin’s width.
If the popup content actually doesn’t fit, it will overflow with a horizontal
scrollbar.

Given that, min-origin is what it sounds like: the popup can be larger than
but not narrower than the origin.


Again it is not clear to me that this should be part of the element API. I’d rather be realizing this example through the Houdini Layout API, using a registered worklet. But I think it’s a good illustration of a use case in this space.

Note that even if one does not consider it valuable to keep attributes concerned with semantics and css properties concerned with style, attributes would not help for items that require interpretation as CSS values (referencing vars, resolving lengths, etc).

@rniwa rniwa closed this as completed Nov 25, 2019
@rniwa
Copy link
Collaborator

rniwa commented Nov 25, 2019

Ugh... didn’t mean to close this.

@rniwa rniwa reopened this Nov 25, 2019
@rniwa
Copy link
Collaborator

rniwa commented Nov 26, 2019

Again it is not clear to me that this should be part of the element API. I’d rather be realizing this example through the Houdini Layout API, using a registered worklet. But I think it’s a good illustration of a use case in this space.

Note that even if one does not consider it valuable to keep attributes concerned with semantics and css properties concerned with style, attributes would not help for items that require interpretation as CSS values (referencing vars, resolving lengths, etc).

These properties are unique in that they’re specific to this custom element. As far as I know, there is no built in CSS property which only applies to a specific element and affects it’s behavior.

But again, this is too close to a particular solution. To keep my analogy to a mode of transportation, this is like saying NYC runs subway 24 hours with specific trains & stations. That’s not really defining the underlying use cases. We need to step back and ask why anyone is using CSS properties to define these things.

@bathos
Copy link

bathos commented Nov 26, 2019

These properties are unique in that they’re specific to this custom element.

The element in question is responsible for a layout effect, so yes — ideally though I could ditch the element, which isn’t used directly but only as an implementation detail for the widgets which are meant for direct consumption. In other words, custom elements are currently providing the only hook I know of for managing custom layout effects. The elements that ‘really’ use these CSS properties are diverse, including select, typeahead, tooltip, and dropdown menu widgets.

I think I did make it very clear why we’re using CSS: the values are CSS values used to realize CSS effects. They need to be able to handle CSS variables and resolve to CSS value types. An attribute cannot take 2em and know if it is a valid length or how to translate it into pixels; an attribute cannot take var(--something) and map it to its local component values; it cannot honor the cascade, or fall back to an initial value, etc.

@chris70c
Copy link
Author

Why anyone is using css properties to define these things is because they don't belong to html attributes, they are used for styling purposes so they should be defined in css and like bathos said some of these properties need to be converted to px from other length units.

@chris70c
Copy link
Author

Another example, some pages let you change language, some languages can be rtl ot bt,
they change the layout via stylesheet so there is no page reload, how can my custom
element respond to that without being notified of the change? Sure in some cases you
can use matches(:dir(rtl)) [when it will be available] but most of the time your
component will need to change the layout, right now as far as I know there is no
solution, you can observe the dir attribute but not the dir attribute on the html element and nothing about tp/bt, observing the relevant styles would solve the problem.

@Jamesernator
Copy link

As a concrete example where I've wanted this is changing internal DOM to change between two styles of math.

For example choosing between this:

Screen Shot 2020-01-07 at 6 22 08 pm

And this:

Screen Shot 2020-01-07 at 6 22 55 pm

Both are semantically equivalent math notation and choosing between them is just a stylistic preference so it would be natural to be able to encode these into the stylesheet e.g.:

math-expression {
  --summation-style: superscript subscript;
}

@media (orientation: portrait) {
  /* Consume vertical space instead on portrait screens */
  math-expression {
    --summation-style: over under;
  }
}

Without observedStyles this is more painful for a consumer of such a component as they will need to explicitly watch the media queries themselves and update an attribute on every element that has such a property. And it's not inherited through shadow roots either unlike custom properties.

@cdata
Copy link
Contributor

cdata commented Jan 7, 2020

I would like to offer another use case for observedStyles.

In the <model-viewer> project, we offer a high-level, declarative API for embedding a 3D model as though it were a rich media type like <img> or <video>. Today, we primarily use attributes for configuration because they are the most practical thing to use, but not all of our attributes "make sense" as attributes.

Consider this basic example of the markup an author may write today:

<model-viewer src="astronaut.gltf"
  alt="An astronaut"
  field-of-view="60deg"></model-viewer>
</model-viewer>

Here the author has configured the src, alt and field-of-view attributes. field-of-view controls the field of view of the virtual camera that is pointed at the model, and amounts to a style in our domain. We would prefer to use a custom property (--field-of-view) to express it, but there is no way for us to efficiently observe changes to such a style so that we can reflect the new value in the 3D scene (internally rendered with <canvas>/WebGL).

This is one example, but we have probably a dozen or so attributes (e.g., camera-orbit, skybox-image, exposure) that make a lot more sense conceptually as styles. The thing we lack is an efficient mechanism to observe changes to a known set of custom properties.

@bathos
Copy link

bathos commented Jan 7, 2020

@cdata Plus CSS has the <angle> type and knows how to parse and reify that production as a useful value whether it was specified with deg, grad, rad, or turn units (or whether it uses calc() or var(), etc...). With registerProperty we can even declare a custom CSS property as <angle> and get all of that processing for free, but we can’t actually use it unless we monitor it in a RAF loop ... and if we use attributes, we’ll either have to artificially restrict the grammar and its power or else end up reimplementing sizable chunks of CSS in JS.

@justinfagnani
Copy link
Contributor

I have the same use case as @Jamesernator for https://github.com/justinfagnani/katex-elements

Right now I vend two separate elements for the "inline" and "display" rendering styles, but this really should be a CSS property, or even better it would just change depending on whether display-outside is inline or block.

Another use-case I've heard from AMP is to control how many images their carousel shows at a time. On narrow screens it may be 1, on wide screens it may be 2 or 3, but this should be styleable by the component user with a media query.

@cdata
Copy link
Contributor

cdata commented Jan 7, 2020

@bathos indeed, and just to underscore the value of that kind of thing to us: we leverage CSS-like units for many of our "style" attributes (lengths, angles and percent), including support for complex expressions like calc(...).

@LeaVerou
Copy link

Just posted a few use cases in w3c/css-houdini-drafts#1010 since I was unaware of this issue.

Examples:

  • Implementing a custom element that works like <fieldset>, where border isn't applied to the whole element, but to the shadow DOM element wrapping the contents.
  • Similarly, a border on a tabs component wouldn't be useful around the whole component, but should set the border of the tabs and the tab panels.
  • To direct where a background declaration should be applied. E.g. right now there are dialog/popup components, where setting the background sets the backdrop color instead of the dialog background because of how they were implemented.
  • To implement high-level custom properties that control multiple declarations, e.g. --tabs-placement: [top | right | bottom | left ], --size: [small | medium | large], --pill: [on | off ] which are currently typically implemented as attributes against TAG guidelines. I linked to Shoelace, but other component libraries are no different, Shoelace just has more linkable docs. :) This is not something that can be solved with ::part() or :state().

Besides propagating declarations to the appropriate child, there is also the opposite: listening to style changes on the children and adjusting ancestors appropriately. E.g. imagine a <pie-chart> component that uses <pie-slice> children to define the different segments, where a shadow <canvas> or conic-gradient background on the parent was used to render the pie chart. This is not implementable without the ability to listen to background changes on <pie-slice> and redraw the chart. And unlike propagating to children, this is not something that can be hacked via inherit and display: contents.

If a generic observer would be too hard, perhaps there could be something specifically for custom elements and slotted elements, like attributeChangedCallback but for CSS properties.

Would something like this be implementable?
I believe (correct me if I'm mistaken) that UAs are not actually listening to changes, but inspecting dirty flags on repaint, which makes implementation for something like this tricky. Would it make it implementable if such an observer explicitly registered a list of elements that should be observed and fired when they are actually repainted, and not the moment the style change happens? I believe this would still cover most use cases.

@jridgewell
Copy link

AMP team is also very interested in this, although we also have use cases that would benefit from a more generic StyleObserver that could be used on any element.

First, AMP has special treatment of display: none Custom Elements. If an element isn't displayed, we forbid element from performing any actions (network requests, click handlers on document, etc). Tied to this is a coordination system that we call the Runtime. Runtime is responsible for telling elements when they can perform actions, eg batching together mutations so that we can avoid mutate->measure cycles. Runtime is also interested in determining when an element is display: none, so that it can skip the mutations on those elements.

AMP runs as a library that's used on anyone's webpage. The HTML Publisher could be using any number of selectors to set display: none on the element, and it's been very difficult for us to automatically detect when one of our elements have their value changed. The best we've been able to do is recommend using the hidden attribute to toggle display (which we can detect via MutationObserver), but this still requires Publisher's to follow our recommendation. We've seen many publishers that are unaware of our recommendation, and they continue to use things like radio button button and :selected selectors to hide tabs, and it's nearly impossible for us to detect these changes. We've fallen back to occasionally doing getComputedStyle calls in a loop in order to detect, but this is laggy and not a great solution.

For a more generic observer, AMP is also interested in position: fixed/sticky elements and their top/bottom values. AMP runs in a complicated iframe setup with some external UI that appears over the iframe (this comes from the parent page). In order to display the Publisher's fixed elements properly, we need to offset the elements by the height of the UI bar. And for the same reasons that display: none is difficult to track, it's nearly impossible for us to detect a change in position value. Because any element can be position fixed, it's just not practical for us to scan the entire document trying to determine if any elements are now fixed position, so just don't try. We can only detect elements that are position: fixed (and not display: none) when the Runtime loads, and we only check those items in the occasional update pass.

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

No branches or pull requests

9 participants