Render function allow force set as attribute instead of property
What problem does this feature solve?
The decision to merge the vnode structure together in Vue 3 render functions has become kind of a pain in the butt for me, because it takes away the ability to explicitly specify a prop as an attribute that should show in the DOM.
https://v3.vuejs.org/guide/migration/render-function-api.html#_3-x-syntax-3
It is necessary in many cases for attributes to be shown in the DOM structure for styling and querying purposes, and not just be set as html element properties that are invisible to DOM querying.
If I'm using Vue on top of web components (window.customElements), it's ultimately up to whether or not that component has the prop defined on the HTML element instance, which means properties like "id", "name" don't show up in the DOM because of this line:
How are we supposed to force something to be set as an attribute instead of a property on an HTML element in Vue 3?
What does the proposed API look like?
I'm not sure how the current decision to merge the definition together in render functions can be fixed to force properties to be added as HTML attributes. If there already is a capability to do this in Vue 3, I totally missed it. The only thing I can think of is have a app.config setting that's a map of HTML element names to properties that should always be attributes and reference that in the render function implementation.
We are also running into this issue when using Ionic Vue, which is a set of Web Components with Vue component wrappers. When setting a Web Component's property on the instance itself, Vue does not render the property as an attribute in the DOM.
Here is a minimal reproduction of the issue outside of Ionic Vue: https://codepen.io/liamdebeasi/pen/RwpoNXE
This example creates two things:
- An
app-test
Web Component with acolor
property set on the instance which defaults to'primary'
. - A
defineContainer
function which wraps the Web Component inside of a Vue component.
In our template, we have the following:
<app-test color="danger">Hello World!</app-test>
I would expect that <app-test color="danger">Hello World!</app-test>
gets rendered in the DOM. If you inspect the DOM you should only see <app-test>Hello World!</app-test>
getting rendered. If you remove the this.color = 'primary';
line in the Web Component constructor, you should see that Vue adds color
as an attribute on the Web Component as expected.
I also tried setting isCustomElement
to include my Web Components, but that did not seem to make a difference.
Setting the property on the Web Component instance is a common practice in tools like Stencil, so it would be great if there was a way for Vue to better handle these cases.
edit: Wanted to provide some context around why this causes issues. In Ionic Vue there is an ion-button
Web Component that lets you set the color
property to change its theme.
In non-Vue environments, developers can target this button in their CSS by doing something like ion-button[color="danger"]
to do some custom styles. In Vue 3, this not possible since the color
property is not rendered as an attribute.
Looking at a possibly related issue (https://github.com/vuejs/vue-next/issues/2343) and it seems like this is the intended behavior, but I am not 100% so it would be great to get some clarification around this.
you can write a custom directive as a workaround see https://github.com/vuejs/vue-next/issues/3738
I don't think adding a directive to every web component instantiation is a good workaround, especially if the entire app is built around those components.
According to Web Component Best Practices it's on the custom element implementation to ensure that DOM properties and element attributes are in sync (when they have primitive values at least).
So if your web components are following best practices, then Vue doing el.color = 'danger'
should be synced to the respective attribute by the web component's implementation.
I presume Stencil does follow those recommendations, they know their stuff, but there's an open issue about a similar/reverse thing to what you are describing, may be worth commenting over there, or opening a new one:
https://github.com/ionic-team/stencil/issues/2701
Edit: I just looked at the Stencil docs (the lib that Ionic web components are written in), and found this – it seems in Stencil, reflecting props to attributes is opt-in, and likely, not enabled in the Ionic components?
Would be interested to understand why that is, they may have good reasons.
So with web components (those that follow best practices) not being a problem, the edge cases where Vue does not add an attribute but you want/need one can – in my opinion – be solved elegantly enough with a custom directive.
I work at the company that manages Stencil, so I can certainly verify with the team 😄 . Will post here when I have more info on that. Also wanted to note that the CodePen I posted in https://github.com/vuejs/vue-next/issues/3792#issuecomment-843549798 is just using a vanilla JS Web Component (no Stencil), so there may be more than one issue.
Good question! So we have reflect
enabled for some but not all of the properties in our components. For example, we have some properties like disabled
in ion-button
that have reflect enabled: https://github.com/ionic-team/ionic-framework/blob/master/core/src/components/button/button.tsx#L50-L63
This lets developers query for certain states (I.e. ion-button[disabled]
). Other props do not have them enabled because they either change frequently or don't really make sense to target.
Re-reading your previous comments, this sounds like something we could easily fix by adding reflect
to the props our developers need to query for, I am mostly just interested in the reasoning behind this behavior so that I can communicate it to our users.
The reasoning for the way the virtualdom handles this is a result of two points, basically:
- With the move to a flat vnode structure, we could unlock important perf and memory usage improvements compared to the Vue 2 vdom. But a flat vdom also means we no longer can differentiate at the vnode level between props meant to be set as props vs. those to be set as attributes.
- We already considered this in the RFC discussions and came to the conclusion that the general rule we use now works well for virtually all scenarios (web components following best practices included):
- When there's a writable DOM prop for the vnode prop, we set as a prop
- When there's not, we set it as an attribute.
- Of course there's a few exceptions and common edge cases we cover for certain special attributes and dom elements, but for "unknown" props this works well from what we found at the time.
We also considered to maybe have special prop pre-suffixes in vnodes like:
{
id: 'my-id'
'myObj:prop': { ... }
}
but that would require parsing each prop name multiple times and would eat up a chunk of he perf improvements we just realized.
Note: The above is a rough outline from memory and likely not completely accurate.
@LinusBorg What about adding a map in the Vue configuration like I discussed? That's zero performance loss if you don't use it, and it gives the application writer control over these scenarios where the library author may or may not make a change. In my perspective, having better control over the way a framework works in order to prevent bugs is much more valuable than a slight performance hit.