-
Notifications
You must be signed in to change notification settings - Fork 546
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
Add a composition API to explicitly expose()
public members
#210
Conversation
Some questions: TS support This would currently make it possible to get the return type of the setup function to determine what properties a ref of a component has: export type ComponentType<
T extends ComponentOptionsBase<any, object, any, any, any, any, any, any>
> = T extends ComponentOptionsBase<any, infer U, any, any, any, any, any, any> ? U : never; for example. I don't know if this is something that is deemed useful but I do think it is. I'm not sure how this would work with the Duplicate calls Proxied Component I/O
|
In Vue things should work both in Composition and Options API, so there should be a fallback to Options API (as suggested here: #135) |
WIP branch for testing: vuejs/core@c6033fb |
One of the problems introduced by this ʻexpose() I considered another solution, which can keep the result of function setup(props, context) {
const count = ref(0)
const inc = () => count.value++
context.proxy({count}) // or: context.proxy('count', count)
context.render(() => JSX)
return {count, inc}
} Or, you can also consider making three APIs, and at the same time clarify the declaration of instance members, and turn the
|
What happens when If multiple "exposes" get merged, I am immediately worried that it will become confusing what are "exposed"; because this means that third party compositions could also expose values? |
Yep that was also my concern. This means that 3rd party compositions may override your own exposed values & the order of the calls matters: hard to debug. Besides this, I think my TS support point still stands. I don't see how this will benefit TS type inference for components once this gets merged. Imagine TS support for Vue gets to the point where we can do this: <template>
<my-component ref="myComponentRef"/>
</template>
<script lang="ts">
// SetupType<MyComponent> could actually infer the return type of `setup`
import MyComponent from "./MyComponent.vue"
const myComponentRef = ref<SetupType<MyComponent>>(); how would such a thing work with |
What if we use a specific symbol in the returned object to denote a public component API? import { componentApi } from 'vue';
// componentApi: symbol
function setup() {
return {
internalState: 42,
[componentApi]: {
publicMethod() { }
}
}
}
|
What about making import { ref } from 'vue'
export default {
setup(_, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
expose({
increment
})
return { increment, count }
}
} |
We need address how exposed properties or functions be added to instance type of the component. |
I agree @jods4 's idea of using a symbol is interesting, but it won't work with @Justineo 's idea of exposing |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Regarding type inference: I'm thinking maybe the exposed type doesn't have to be somewhat provided via the component type itself - if we can support named type exports from SFCs. For example: <!-- Foo.vue -->
<script setup lang="ts">
import { ref, Ref, defineOptions } from 'vue'
const { expose } = defineOptions()
const count = ref(0)
// public API
export interface API {
count: number
}
expose<API>({ count })
</script> In another file: <!-- Bar.vue -->
<script lang="ts">
import { ref, watchEffect } from 'vue'
import Foo, { API as FooAPI } from './Foo.vue'
const foo = ref<FooAPI | undefined>()
watchEffect(() => {
console.log(foo.value && foo.value.count)
})
</script>
<template>
<Foo ref="foo"/>
</template> This also already works for non-SFC components written in TS or TSX. |
This should work with TS plugin too. |
Now that <script setup>
import { ref } from 'vue'
export const count = ref(0)
export function increment() {
count.value++
}
</script> The sfc compiler would aggregate all the exported binding using ref: count = 0
export { count } If this is an issue, a shortcut could be also provided: export_ref: count = 0 |
For setup + render functions the ideal implementation would be something like this: import { defineComponent, ref, render } from 'vue'
export default defineComponent({
setup (props) {
const publicState = ref('public')
const privateState = ref('private')
return {
publicState,
[render]: () => <div>{publicState.value}, {privateState.value}</div>
}
}
})
The current |
The
For this case I think you would rely on the setup(){
const foo = ref()
expose({foo})
return ()=> h(div, {ref: foo})
} |
To get the // Render function
defineComponent({
setup(props, { render }) {
const foo = ref('public')
const bar = ref('private')
render(() => <div>{foo.value}, {bar.value}</div>)
return {
foo,
}
}
})
// Template
<div>{{ foo }}, {{ bar }}</div>
defineComponent({
expose: ['foo'], // Only foo is available externally
setup(props) {
const foo = ref('public')
const bar = ref('private')
return {
foo,
bar,
}
}
})
// External use
setup () {
const renderComponent = ref()
const templateComponent = ref()
onMounted(() => {
renderComponent.value.foo // 'public'
renderComponent.value.bar // undefined
templateComponent.value.foo // 'public'
templateComponent.value.bar // undefined
})
return () => (<>
<RenderComponent ref={renderComponent} />
<TemplateComponent ref={templateComponent} />
</>)
}
It's about the child, if |
The way I see it is: If you explicit expose something you want to prevent something else, because by default everything is accessed via the |
Isn't this part of the motivation of the |
There's a
I think is expected a vue component to expose those |
Altho the use case: defineComponent({
expose: ["process"],
data(){
return {
internalData: 1
}
},
methods: {
process() {
this.internalData++;
}
}
}) In this case I would expect the public instance only allow access to The typing for it: vuejs/core#3399 |
Thank @CyberAP, I saw the implementation on the vue-next and thought it was this RFC, my bad. I think this two RFC are closely related, because the internal behaviour will be the same, only the declaration API is different. They can even work together for declaring the typescript typing: // no expose typing
defineComponent({
setup(){
expose({test: 1})
}
})
// with expose typed
defineComponent({
expose: undefined as { test: number},
setup(){
expose({ test: 1})
}
})
// equivalent
defineComponent({
expose: ['test']
setup(){
return { test: 1 }
}
})
// or options
defineComponent({
expose: ['test'],
data(){
return {
test: 1
}
}
}) |
Can it support using with SFC State-driven CSS Variables (v-bind in <style>)? e.g. <script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, { expose }) {
expose({
color: 'red',
font: {
size: '2em',
},
})
return () => <div class='text'>Hello</div>
},
})
</script>
<style>
body {
background: #000;
text-align: center;
}
.text {
color: v-bind(color);
/* expressions (wrap in quotes) */
font-size: v-bind('font.size');
}
</style> Currently, the above code won't work. |
This will be a great addition in 3.1.3!! There's one semi-related thing that will need some work: consuming TS types. Let's assume that the type of your component API is manually or automatically exported from the SFC. Consuming this is non-trivial:
I suppose we would need some kind of TS plugin that can be plugged into the compilation. Related: vuedx/languagetools#228 |
Closing in favor of #343 |
Rendered