Events interoperability proposal
See original GitHub issueAfter some discussion/back and forth in Discord, I thought it might be a better idea to express y concerns/ideas here in a more descriptive way; this is a continuation of the following message from your Discord server.
For brevity and for the sake of specificity, this proposal and its examples will be based around Vue (and v3 to be more specific).
Problem
As we know Mitosis allows us to write component in the “lower common denominator” for frameworks such as React, Vue, Svelte, etc. This is such a power that we didn’t have before. Nonetheless, I think that there’s some room for improvement regarding how Vue components are generated in general (and maybe this related to other frameworks?).
It’s not uncommon for a Design System to have components such as modals or more involved input elements with different elements inside them, (div
+ label
+ possible error message) so that it’s easier for developer to use those buildings blocks as a whole. Nonetheless, right now a component like the following written in Mitosis:
import { Show } from "@builder.io/mitosis";
interface Props {
name: string;
label: string;
onChange: (event) => void;
errorMessage?: string;
}
export default function SuperInputField(props: Props) {
return (
<div class="my-super-duper-input-field">
<label for={props.name}>{props.label}</label>
<input
id={props.name}
name={props.name}
onChange={(event) => props.onChange(event)}
/>
<Show when={props.errorMessage}>
<p class="form-message danger">{props.errorMessage}</p>
</Show>
</div>
);
}
Gets converted to this equivalent Vue 3 (composition API):
<template>
<div class="my-super-duper-input-field">
<label :for="name">{{ label }}</label>
<input :id="name" :name="name" @input="onChange($event)" />
<template v-if="errorMessage">
<p class="form-message danger">{{ errorMessage }}</p>
</template>
</div>
</template>
<script setup>
const props = defineProps(["name", "label", "onChange", "errorMessage"]);
</script>
Which looks fine at first glance but presents a few problems:
- In frameworks similar to React (Solid, etc) it’s common to pass functions to be executed by the child components “whenever”. But this is not the case with Vue. Vue standard way of doing this is through events between parent and child. Meaning that the correct code for this specific output should be:
<template>
<div class="my-super-duper-input-field">
<label :for="name">{{ label }}</label>
<input :id="name" :name="name" @input="emit('change', $event)" />
<template v-if="errorMessage">
<p class="form-message danger">{{ errorMessage }}</p>
</template>
</div>
</template>
<script setup>
const props = defineProps(["name", "label", "errorMessage"]);
const emit = defineEmits(['change']);
</script>
NOTE the new
const emit = defineEmits(['change']);
and the@input
event in the template (which shouldn’t it be@change
; since that was the one we listened to in the original Mitosis component?
- Furthermore, with Vue we have things like
v-model
which allows for two way data binding which is really common on form elements and what not. The problem with the current approach is that this would work on the majority of frameworks that follow the “React” way, but not for those that communicate between parent-child components via events such as Vue. For Vue and more specific Vue 3, to make a “custom component” work with thev-model
directive, that component must acceptmodelValue
as a prop and emitupdate:modelValue
this allows for this usage:<SuperInputField v-model="value" />
which gets expanded by Vue to:<SuperInputField :model-value="value" @update:modelValue="newValue => value = newValue" />
- Similar to this, Vue allows to have multiple
v-model
directives right now withv-model:title="title"
as long as the component accepts a prop namedtitle
and emits an event namedupdate:title
with the new value for it.
- Similar to this, Vue allows to have multiple
The problems mentioned above might be “too specific” to Vue or similar frameworks like Svelte which also allows for custom event dispatching. But I really think that nailing this or a subset of this in Mitosis can go a long way for the adoption of the tool to write performant and seamless design systems that just work with it. And specially more so with Vue as it’s one of the “main” frameworks out there.
Proposed solutions/ideas
I would propose to tackle this issue in a two way approach maybe?
Let me elaborate a little bit into what I mean by this.
- My first try would be to map the events in props like
onClick
,onInput
,onSomething
and automatically convert them to listeners like@click
,@input
,@something
as they should. This would get us the majority of the way there in terms of pairity with Vue. - My second try would be taking a stab at the
v-model
implementation, but I would leave this out of the “main compiler” and expose it as a plugin (maybe?) that people can use withuseMetadata
passing some configuration that then the plugin would map correctly to the different outputs, specially Vue and its variants.
Example of point #2
Given this component:
import { Show, useMetadata } from "@builder.io/mitosis";
interface Props {
name: string;
label: string;
value: any;
onUpdateValue: (event) => void;
errorMessage?: string;
}
useMetadata({
vModel: {
modelValue: 'value',
events: {
'update:modelValue': 'onUpdateValue',
},
},
})
export default function SuperInputField(props: Props) {
return (
<div class="my-super-duper-input-field">
<label for={props.name}>{props.label}</label>
<input
id={props.name}
name={props.name}
value={props.value}
onChange={(event) => props.onUpdateValue(event.target.value)}
/>
<Show when={props.errorMessage}>
<p class="form-message danger">{props.errorMessage}</p>
</Show>
</div>
);
}
And a config (mitosis.config.js
) file like:
const { vModelPlugin } = require('@builder.io/mitosis/vue'); // <- or whatever
module.exports = {
// ...rest configuration
plugins: [
// ... rest of plugins
vModelPlugin
];
};
Mitosis would generate the following output for Vue:
<template>
<div class="my-super-duper-input-field">
<label :for="name">{{ label }}</label>
<input :id="name" :name="name" :value="value" @input="emit('update:modelValue', $event.target.value)" />
<template v-if="errorMessage">
<p class="form-message danger">{{ errorMessage }}</p>
</template>
</div>
</template>
<script setup>
const props = defineProps(["name", "label", "value", "errorMessage"]);
const emit = defineEmits(['update:modelValue']);
</script>
With point #1
we would allow better parity with Vue out of the box. And with point #2
we would make the “feature/implementation” an opt-in for the developers based on their targets and actual requirements.
Closing
As mentioned before, having the #1
point working out of the box with Mitosis I think should be a “must” as that falls (IMHO) under the “lowest common denominator” category; and the vModelPlugin
would be a layer on top of that.
I would be more than happy to try and discuss this further and even try and help with moving this forward in terms of implementation.
Thank you! 🙏 💪 🚀
Issue Analytics
- State:
- Created a year ago
- Reactions:5
- Comments:7 (5 by maintainers)
Top GitHub Comments
I’m interested in this proposal, it’s really necessary in vue, looking forward to this proposal will implement 💪
@samijaber I know you’re super busy. But is there anything else I can do to continue the conversation?