Open bjufre opened 1 year ago
First, thanks a lot for laying out this well-written proposal. I appreciate that we have more and more folks like yourself laying out ideas around how we can improve Mitosis 😄. I'm catching up on notifications after a very busy few weeks, so I also appreciate your patience.
- 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.
This is already the case! Event handlers all get mapped to these listeners. See this example. It's done in this code: https://github.com/BuilderIO/mitosis/blob/345852b71e77e604ab87e2d27c2ba51a0cf16e8a/packages/core/src/generators/vue/blocks.ts#L302-L309
Are you seeing something done differently for these event listeners? If so, that's most likely a bug 🤔
My first thought is that I would prefer that the outputs generated by Mitosis all have the exact same API for consistency. This means that a component would have the same props, whether it's the React, Qwik, Svelte or Vue version. But you make a good point that Vue has this neat API around allowing parents to add event handlers there, so why not take advantage of that. It will be more natural to folks consuming your Vue components since that's the convention there.
I'm down to try your idea out and see if folks find it valuable! I will add an explanation in a follow-up comment in this PR for anyone who'd be interested in implementing this (whether yourself or someone else), outlining what changes need to be made to Mitosis (& where).
@bjufre I have some ideas around this, but first I need one important piece: can you give me an example of how this SuperInputField
example would be consumed in Vue? i.e. share with me a Vue 3 component that would be a parent of SuperInputField
, and how it would provide event handlers.
That would fill the knowledge gap for me and help lay out what needs to be done to support this (if it's possible!)
@samijaber first of all thank you for your patience; I've been dealing with some crazy stuff at work and I had to deal with it and put on hold any other efforts to move the project that I would use Mitosis for.
Secondly thank you for the thorough response, I will do my best to provide a good answer to try and spark the discussion further.
I think that a possible good example is to showcase how a composable Modal
component might be created and used for easy to use later on within an engineering team. One might create a Modal
component, but at the same time, in order to limit and constraint further how the design system is used, we might introduce a ConfirmModal
that would encapsulate the common setup for this kind of scenarios; this might look something like:
Confirmation Modal
Given this Mitosis component:
import Modal from './components/Modal';
import Button from './components/Button';
interface Props {
show: boolean;
children: any; // <- What's the best way to type this with Mitosis?
cancelLabel?: string;
confirmLabel: string;
onCancel: () => void;
onConfirm: () => void;
}
export default function ConfirmationModal(props: Props) {
return (
<Modal
show={prop.show}
type="confirm"
slotFooter={(
<div class="d-flex justify-between align-center">
<Button type="ghost" onClick={props.onCancel}>{props.cancelLabel || 'Cancel'}</Button>
<Button tone="danger" onClick={props.onConfirm}>{props.confirmLabel}</Button>
</div>
)}
>
{props.children}
</Modal>
)
}
I would expect the following Vue output:
<template>
<Modal type="confirm" :show="show">
<slot />
<template #footer>
<div class="d-flex justify-between align-center">
<Button type="ghost" @click="emit('cancel')">{{ cancelLabel }}</Button>
<Button tone="danger" @click="emit('confirm')">{{ confirmLabel }}</Button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import Modal from './components/Modal';
import Button from './components/Button';
interface Props {
children: any;
cancelLabel: string;
confirmLabel: string;
}
const props = defineProps<Props>;
const emit = defineEmits<{
(e: 'confirm'): void;
(e: 'cancel'): void;
}>;
</script>
Note that the types for the emit
are derived from the original Mitosis props.
Usage:
<template>
<!-- ...Rest of the page -->
<!-- Some "entity actions" -->
<div class="actions">
<Button type="primary" @click="openConfirm">Delete item</Button>
</div>
<!-- Our super modal -->
<ConfirmModal :show="showConfirm" confirmLabel="Delete" @cancel="closeConfirm" @confirm="deleteEntity">
Are you sure that you want to delete this entity?
</ConfirmModal>
</template>
<script setup lang="ts">
import Button from '@design-system/vue/Button';
import ConfirmModal from '@design-system/vue/ConfirmModal';
import { ref } from 'vue';
const showConfirm = ref<boolean>(false);
function openConfirm() {
showConfirm.value = true;
}
function closeConfirm() {
showConfirm.value = false;
}
</script>
In this example we've used some composition between "supposed" components inside the same design system created with Mitosis. And it shows why it might be important to use event emitters/handlers instead of rely in prop drilling.
I'm interested in this proposal, it's really necessary in vue, looking forward to this proposal will implement 💪
The idea is so great,Event listener is more convient for vuer.
@samijaber I know you’re super busy. But is there anything else I can do to continue the conversation?
@samijaber, Are you planning to go forward with implementation ?
Hey folks,
I've been holding off on looking into this proposal due to its highly complex nature, relative to the small benefits I see.
The complexity I envision comes from us potentially needing:
@event
syntax instead of providing an onEvent
prop.PS: I have other priorities at the moment that prevent me from working on this. I am always willing to outline the work needed to get it done, but even that would be a decent chunk of non-trivial new architecture. The good news is that we are currently solving the second bullet point (analyzing an entire Mitosis project's TypeScript types) for another big task (implementing fine-grained reactivity/signals across frameworks). So once we have an example of how to do that, it will definitely make this proposal easier to tackle. 🙏🏽
What I'm stumped on are the benefits of this approach. Again, I am not a Vue expert, so I might totally be missing something here, but I don't understand what these events offer that you can't do with prop drilling (besides maybe some syntactic sugar). I definitely understand that it might not be idiomatic Vue code though.
I looked at @bjufre and it feels like prop drilling would still work there...so my follow-up question is:
what can you do with Vue custom events that is impossible to do without them?
I would highly appreciate examples of Mitosis output that show such limitations.
Because as it currently stands, Vue's custom events look like a "better alternative to prop drilling" to me, but it doesn't unlock any new abilities. And I'd rather spend the limited resources available to make Mitosis cover more ground (like using signals/reactive stores, support CSS file imports, etc.).
Because as it currently stands, Vue's custom events look like a "better alternative to prop drilling" to me, but it doesn't unlock any new abilities.
I'd say you're generally right about that, but the events-approach is the de-facto standard in Vue. All the docs etc do not use prop drilling, but the event approach. As a result, Vue users for example expect components from component libraries to also follow this approach, using prop drilling would feel/be just ... weird. Furthermore there is also quite a lot syntactic sugar for using the event-approach (also used in docs etc.)
But I totally get that this is a really hard problem to solve for a compiler like mitosis while maintaining a functionally equal output for all frameworks and that the limited resources might be spend on other topics
It is not only about Vue it is also about Angular. They also have the concept of Outputs.
I suggest useOutputs
to define all its outputs. For Frameworks like vue and angular it will be transpiled in vue and angular to their output / emit code. In Frameworks like React, we would need to merge it with the props. We could use the on...
convention to add them.
The API was inspired by Vue Instead of defineEmits it uses useOutputs.
import { useOutputs } from '@builder.io/mitosis';
import { ChangeEvent, InputEvent } from './ofa-text-input.lite';
export default function MyModal(props: any) {
const outputs = useOutputs<{
userNameChange: [name: string]
cancel: void
confirm: void
}>()
return (
<div>
<h1>Enter username you want to delete. </h1>
<input onChange={(event) => outputs.userNameChange(event.target.value)} />
<button onClick={() => outputs.canel()}>Cancel</button>
<button onClick={() => outputs.confirm()}>Confirm</button>
</div>
);
}
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'my-modal',
template: `
<div>
<h1>Enter username you want to delete.</h1>
<input (input)="onUserNameChange($event)" />
<button (click)="onCancel()">Cancel</button>
<button (click)="onConfirm()">Confirm</button>
</div>
`,
})
export class MyModal {
@Output() userNameChange = new EventEmitter<string>();
@Output() cancel = new EventEmitter<void>();
@Output() confirm = new EventEmitter<void>();
onUserNameChange(event: Event) {
this.userNameChange.emit((event.target as HTMLInputElement).value);
}
onCancel() {
this.cancel.emit();
}
onConfirm() {
this.confirm.emit();
}
}
<my-modal (userNameChange)="handleUserNameChange($event)" (cancel)="handleCancel()" (confirm)="handleConfirm()"></my-modal>
<script setup lang="ts">
import { ref, defineEmits } from 'vue';
const emit = defineEmits<{
(e: 'userNameChange', name: string): void;
(e: 'cancel'): void;
(e: 'confirm'): void;
}>();
function onChange(event: Event) {
emit('userNameChange', (event.target as HTMLInputElement).value);
}
function onCancel() {
emit('cancel');
}
function onConfirm() {
emit('confirm');
}
</script>
<template>
<div>
<h1>Enter username you want to delete.</h1>
<input @change="onChange" />
<button @click="onCancel">Cancel</button>
<button @click="onConfirm">Confirm</button>
</div>
</template>
<MyModal @userNameChange="handleUserNameChange" @cancel="handleCancel" @confirm="handleConfirm" />
Disclaimer: I Partly used OpenAI to generate the transpiled output. Read more: Here is an OpenAI-generated list of some other frameworks that I am not familiar with: https://chatgpt.com/share/98213b24-3e00-46a0-a740-d43d79ff45fb
After 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.
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:Gets converted to this equivalent Vue 3 (composition API):
Which looks fine at first glance but presents a few problems:
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" />
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.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.
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.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:
And a config (
mitosis.config.js
) file like:Mitosis would generate the following output for Vue:
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 thevModelPlugin
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! 🙏 💪 🚀