Not able to pass object as attr with stencil.js generated web component
Version
3.0.0
Reproduction link
https://github.com/robaxelsen/stencil-vue-3-repro
Steps to reproduce
- Go to http://rob.ee/dev/stencil-vue-3-repro/ to see reproducer code live
- See that first line which should render the object stringified, returns "[object Object],[object Object]" instead of stringified object
What is expected?
Should return stringified object [{"id":"id1","label":"Option 1"},{"id":"id2","label":"Option 2"}]
instead of "[object Object],[object Object]"
. The line below is a "hand rolled" web component that does the same successfully. Difference is that the first one is made with Stencil.js.
What is actually happening?
Web component renders "[object Object],[object Object]"instead of the stringified object
[{"id":"id1","label":"Option 1"},{"id":"id2","label":"Option 2"}]`.
This example works if we instead use Vue 2 with object.prop
that since have been deprecated. Does not work with Vue 3.
Source code for the live reproducer is here: https://github.com/robaxelsen/stencil-vue-3-repro
Most relevant are these files/folders:
- Index.html where web component is used with Vue 3: https://github.com/robaxelsen/stencil-vue-3-repro/blob/main/www/index.html
- Stencil.js compiled web component js files: https://github.com/robaxelsen/stencil-vue-3-repro/tree/main/www/build
We have not been able to isolate whether this is a Stencil.js or Vue 3 issue, but seems it works with Vue 2 I am posting here first. Will also post issue in Stencil.js repo next, and link to this.
the issue is likely rooted in the fact that, similar to React, in our new virtual DOM design, we don't differentiate between, (Vue-)props, attributes and domProps (or events) - all are just propertiess on a flat vnode object.
We handle this for normal DOM Element props/attributes internally during the patch phase, but it seems to fail here for custom elements that rely on objects being passed via properties rather than as attributes (which are always string values).
Interested to understand how React et. al. void this.
Edit: re-reading your OP got me doubting my understanding, likely because of a lack of familiarity with stencil.
You say you expect a stringified object to be passed. Does stencil not use some props to pass objects without the need of being stringified?
@LinusBorg Thanks for having a look and providing insights!
Interested to understand how React et. al. void this.
They don't handle it, I am afraid. From https://custom-elements-everywhere.com/: "React passes all data to Custom Elements in the form of HTML attributes. For primitive data this is fine, but the system breaks down when passing rich data, like objects or arrays. In these instances you end up with stringified values like some-attr="[object Object]" which can't actually be used."
Edit: re-reading your OP got me doubting my understanding, likely because of a lack of familiarity with stencil. You say you expect a stringified object to be passed. Does stencil not use some props to pass objects without the need of being stringified?
No, that's my mistake. You understood me correctly initially, I think. In my limited understanding of Stencil.js internals, I believe they do not accept stringified objects, but require the framework or vanilla JS implementer to pass it as a prop instead for objects and arrays. From https://custom-elements-everywhere.com/: "By default, Vue passes all data to Custom Elements as attributes. However, Vue also provides syntax to instruct its bindings to use properties instead. To bind to a Custom Element property use :foo.prop="bar"."
In the past (Vue 2) we were able to rely on .prop
as our savior for these cases. Would a reintroduction of .prop
be feasible, or does it have performance or maintainability costs that want to be avoided?
EDIT: From Stencil.js' side, there exists output targets that wrap all web components with framework specific bindings. Angular and React exists, but Vue.js hasn't been implemented yet: https://github.com/ionic-team/stencil-ds-output-targets
This wasn't an issue for Stencil.js with Vue 2 though, as .prop
works well without Vue specific output target and just plain Stencil.js web components.
It seems significant that the "vanilla" custom element doesn't exhibit the same behavior–this would seem to point to a problem in Stencil.
I confirmed that a vanilla element seems to work fine here: https://stackblitz.com/edit/vue-rxb2du?file=src%2FApp.vue
Notably, it wasn't even necessary to use .prop
in the binding:
<template>
<div id="app">
<img alt="Vue logo" src="https://vuejs.org/images/logo.png">
<data-element :data="data"></data-element>
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
Thanks @graynorton. Yeah, hoping for an answer from the Stencil.js team too.
But also curious as to if this could have to do with state management patterns inside more complex web components, or some similar best practices across web components frameworks, due to custom-elements-everywhere.com and their mention of Vue web component support depending on .prop for handling these type of cases (see quote and reference above).
@robaxelsen It doesn't look like the tests for custom-elements-everywhere have been updated for Vue 3.0 yet, so I'd guess that the descriptive text hasn't been either.
I haven't looked at the Vue source and would be curious what @LinusBorg has to say, but it appears to me that Vue is no longer passing data to custom elements using attributes by default. What's unclear without digging in is whether it's always passing data using properties (which is probably not great), or whether it's doing something fancy based on either the type of data being passed or the existence of a like-named property on the target element.
I was curious, so played around a bit more.
It appears that when binding into a CE, Vue 3.0 will bind to a property if the CE has a property descriptor or a setter for the corresponding name; otherwise, it will bind to an attribute. This seems like a good behavior; it essentially means that Vue will prefer binding to a CE property and fall back to an attribute as needed.
@robaxelsen, based on your broken example, it looks to me like Vue's logic to check for the existence of a property is failing for Stencil-created CEs (for whatever reason) and Vue is therefore binding to the attribute instead. Possibly this could be addressed by either Vue or Stencil, but someone will have to dig in and figure out specifically why Vue 3.0 isn't recognizing properties on Stencil CEs.
Vue just uses key in el
to determine if it should bind to a property: https://github.com/vuejs/vue-next/blob/51c18ed193285b593adbf909bf323a645de246e7/packages/runtime-dom/src/patchProp.ts#L112
Stencil doesn't define the data
property until after vue has already checked if it exists: https://github.com/ionic-team/stencil/blob/f6437b384f0c65c3bc9edaa06dfc36deb998aefb/src/runtime/proxy-component.ts#L20
If you place a breakpoint here you'll see that the stencil component is in the DOM but hasn't initialised itself yet so doesn't display any content.
Stencil manages the loading of components and by default it does so lazily. @KaelWD comment makes sense. I suspect if you were to change to a custom element with no lazy-loading it'd work fine (https://stenciljs.com/docs/custom-elements)
Stencil has released a new output target dist-custom-elements
in version 2.4.0 that allows loading of the custom element without stencil lazy loading them. I've found that by using these imports with the .prop
modifier in Vue that it appears to work with Vue 3 in regards to passing objects or arrays.
the .prop doesnt send the data on update...
EDIT: it work if I am spreading the object inside { ...myObj } that sounds like a non normal behavior in vue3 at least with reactive