Skip to content

Commit 6d023bb

Browse files
authored
Expose API (#343)
1 parent b20f29a commit 6d023bb

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed

active-rfcs/0042-expose-api.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
- Start Date: 2020-09-06
2+
- Target Major Version: 3.x
3+
- Reference Issues: #135, #210
4+
5+
# Summary
6+
7+
Provide the ability for components to control what is publicly exposed on its component instance. This proposal unifies #135 and #210 with additional details.
8+
9+
# Basic example
10+
11+
## Options API
12+
13+
```js
14+
export default {
15+
expose: ['increment'],
16+
17+
data() {
18+
return {
19+
count: 0
20+
}
21+
},
22+
methods: {
23+
increment() {
24+
this.count++
25+
}
26+
}
27+
}
28+
```
29+
30+
## Composition API
31+
32+
```javascript
33+
import { ref } from 'vue'
34+
35+
export default {
36+
setup(props, { expose }) {
37+
const count = ref(0)
38+
39+
function increment() {
40+
count.value++
41+
}
42+
43+
// public
44+
expose({
45+
increment
46+
})
47+
48+
// private
49+
return { increment, count }
50+
}
51+
}
52+
```
53+
54+
Here in the both cases, other components would only be able to access the `increment` method from this component, and nothing else.
55+
56+
# Motivation
57+
58+
In Vue, we have a few ways to retrieve the "public instance" of a component:
59+
60+
- Via template refs
61+
- Via the `$parent` or `$root` properties
62+
63+
Up until now, the concept of the "public instance" is equivalent to the `this` context inside a component. However, this creates a number of issues:
64+
65+
1. A component's public and internal interface isn't always the same. A component may have internal properties that it doesn't want to expose to other components. In other cases a component may want to expose methods that are specifically meant to be used by other components.
66+
67+
2. A component returning render function from `setup()` encloses all its state inside the `setup()` closure, so nothing is exposed on the public instance, and there's currently no way to selectively expose properties while using this pattern.
68+
69+
3. `<script setup>` is also compiled into (2) so has the same issue.
70+
71+
The proposed APIs solve these issues by giving components the ability to explicitly declare publicly exposed properties.
72+
73+
# Detailed design
74+
75+
## Options API
76+
77+
A new option, `expose` is introduced. It expects an array of property keys to be exposed:
78+
79+
```js
80+
// Child.vue
81+
export default {
82+
expose: ['increment'],
83+
84+
data() {
85+
return {
86+
count: 0
87+
}
88+
},
89+
methods: {
90+
increment() {
91+
this.count++
92+
}
93+
}
94+
}
95+
```
96+
97+
In a parent component:
98+
99+
```html
100+
<Child ref="child" />
101+
```
102+
103+
```js
104+
this.$refs.child.increment() // ok
105+
this.$refs.child.count // undefined
106+
```
107+
108+
If `expose` is an empty array, then the component would be considered "closed" and no properties would be exposed.
109+
110+
> **Note:** `expose` option is only respected in the base component and ignored when used in mixins.
111+
112+
## Composition API
113+
114+
In Composition API, the 2nd argument of `setup` (aka the "setup context") now also provides the `expose` method:
115+
116+
```js
117+
import { ref } from 'vue'
118+
119+
export default {
120+
setup(props, { expose }) {
121+
const count = ref(0)
122+
123+
function increment() {
124+
count.value++
125+
}
126+
127+
// public
128+
expose({
129+
increment
130+
})
131+
132+
// private
133+
return { increment, count }
134+
}
135+
}
136+
```
137+
138+
The `expose` method expects an object of the actual values to be exposed.
139+
140+
### In `<script setup>`
141+
142+
The `defineExpose()` macro compiles into runtime `expose()` call.
143+
144+
```html
145+
<script setup>
146+
const count = ref(0)
147+
148+
function increment() {
149+
count.value++
150+
}
151+
152+
defineExpose({
153+
increment
154+
})
155+
</script>
156+
```
157+
158+
See [relevant section](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md#exposing-components-public-interface) in the `<script setup>` RFC.
159+
160+
### Exposed Ref Unwrapping Behavior
161+
162+
Note the exposed instance unwraps refs similar to the normal public instance:
163+
164+
```js
165+
// in child
166+
const count = ref(0)
167+
expose({
168+
count
169+
})
170+
```
171+
172+
```js
173+
// in parent
174+
this.$refs.child.count // 0
175+
```
176+
177+
## Limiting Expose Control to Base Component
178+
179+
As raised in the discussions for a previous draft (#210), it would be very confusing if mixins and external composition functions are able to expose arbitrary properties. Therefore, the API is designed to restrict the capability to the base component only:
180+
181+
- The `expose` option is ignored when encountered in `mixins` or `extends` sources.
182+
- The Composition API `expose` function is provided via the setup context instead of a global API (so that it cannot be imported in external composition functions) and can only be called once.
183+
184+
## Additionally Exposed Properties
185+
186+
An experimental implementation has been shipped in prior versions and we found in some cases there are patterns or tools (e.g. `vue-test-utils`) that relies on the presence of Vue built-in instance properties like `$el` or `$refs`.
187+
188+
The public instance with explicit `expose` thus still exposes these built-in instance properties.
189+
190+
# Drawbacks
191+
192+
N/A
193+
194+
# Alternatives
195+
196+
N/A
197+
198+
# Adoption strategy
199+
200+
This feature is additive and doesn't affect existing usage.
201+
202+
# Unresolved Questions
203+
204+
## Type Inference
205+
206+
Currently no Vue tooling has the ability to infer types of child component instances obtained from a ref.
207+
208+
A workaround is to explicitly export the public interface from the child component:
209+
210+
```html
211+
<!-- Child.vue -->
212+
<script lang="ts">
213+
export interface Api {
214+
// ...
215+
}
216+
</script>
217+
```
218+
219+
In parent:
220+
221+
```html
222+
<script setup lang="ts">
223+
import { ref } from 'vue'
224+
import Child from './Child.vue'
225+
import type { Api } from './Child.vue'
226+
227+
const childRef = ref<Api>()
228+
</script>
229+
230+
<template>
231+
<Child ref="childRef" />
232+
</template>
233+
```
234+
235+
The above does not affect the runtime API design and therefore should not block this RFC from being merged.

0 commit comments

Comments
 (0)