11

New to Vue in general and currently using 3.2.37, I possibly missunderstood the correct usage of composition api’s defineExpose directive as documented here: https://vuejs.org/api/sfc-script-setup.html#defineexpose

There are also some threads to be found, explaining how to expose members from inside <script setup> like Calling method on Child Component - Composition API. But somehow I cannot manage to expose the version constant ref from child component HelloWorld so that it's version can be interpolated in app component.

app.vue:

<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <main>
    <HelloWorld />
    <h1>version:{{HelloWorld.version}}</h1>
  </main>
</template>

HelloWorld.vue:

<script setup>
import { ref } from 'vue';

const version = ref('1.0.0');

defineExpose({ version });
</script>

<template>
    <div>
        <h3> You’ve successfully created a project with Vue.js and Vuetify. </h3>
    </div>
</template>

Image: version 1.0.0 not showing

5 Answers 5

19

defineExpose() exposes properties on a component's template ref, not on the component definition imported from the .vue file.

<App> could use a computed prop to access <HelloWorld>'s exposed version prop:

  1. Apply a template ref on <HelloWorld>:
<!-- App.vue -->
<HelloWorld ref="helloWorldRef" />
  1. Create a ref with the same name as the template ref:
<!-- App.vue -->
<script setup>
import { ref } from 'vue'

const helloWorldRef = ref(null)
</script>
  1. Create a computed prop that returns <HelloWorld>'s exposed version prop. Make sure to use optional chaining (or a null-check) on helloWorldRef.value, as it's initially null:
<!-- App.vue -->
<script setup>
import { computed } from 'vue'

const version = computed(() => helloWorldRef.value?.version)
</script>

<template>
  ⋮
  <h1>version:{{ version }}</h1>
</template>

demo

Sign up to request clarification or add additional context in comments.

Comments

13

you can check this website...

Components using Options API or Composition API without are open by default.

If we try to access the public instance of such a component via template ref or $parent chains, it will not expose any of the bindings declared inside the block.

We can use the defineExpose compiler macro to explicitly expose properties:

Child.vue

<script setup>
import { ref } from 'vue'
const foo = ref('foo')
const bar = ref('bar')
defineExpose({
  foo,
  bar,
})
</script>

When a parent gets an instance of Child.vue via template ref, the retrieved instance will be of the shape { foo: string, bar: string } (refs are automatically unwrapped just like on normal instances):

Parent.vue

<template>
  <Child ref="child" />
</template>
<script setup>
import { ref } from 'vue'
const child = ref(null);

onMounted(() => {
  console.log(child.value.foo)
  console.log(child.value.bar)
})

</script>

1 Comment

It's long ago, I asked. Thank You all, for the detailed explainations. @Mohammad: This answer here was exactly what I was searching for.
3

Adding to the previous answers, here is how you can do it with Typescript

Exposing component

<script setup lang="ts">
// ...
const foo = computed(() => /* ... */);
defineExpose({ foo });
</script>

Consuming component

<template>
  <!-- the ref name has to be the same as in the script area -->
  <MyComponent ref="bar"></MyComponent>

  <!-- bar.foo will be correctly typed and also auto suggested -->
  <div v-if="bar?.foo"></div>
</template>
<script setup lang="ts">
// ...

// Notice InstanceType<typeof ...>
const bar = ref<InstanceType<typeof MyComponent>>();
</script>

1 Comment

The need for InstanceType can't be stressed enough. With ref<typeof MyComponent>, all properties will have type any, with no warning. Thanks!
1

An object of DOM elements and component instances, registered via template refs.

You can do it this way.

<HelloWorld ref="hello" />
<h1>version:{{ $refs.hello.version }}</h1>

1 Comment

Thank you, that results in: App.vue:10 Uncaught TypeError: Cannot read properties of undefined (reading 'version')
0

If you want to access the root element of a component, you can use ref + emit + watch. And if you need to pass a component up the tree, you can use emit in a similar way.

Child component

// BaseButton component

<template>
  <button
    ref="buttonElement"
    type="button"
  >
    <slot />
  </button>
</template>

<script setup lang="ts">
import { ref, watch, type Component } from 'vue';

/** Emit */
const emit = defineEmits<{
  (event: 'update:rootComponent', value: Component | null): void;
}>();

/** Button element */
const buttonElement = ref<HTMLElement | null>(null);

// watch the button element
watch(buttonElement, (value) => {
  emit('update:rootComponent', value);
});
</script>

Parent component

// Any component where the BaseButton is needed

<template>
  <BaseButton @update:root-component="(value) => (buttonRef = value)">
    {{ 'Some text' }}
  </BaseButton>
</template>

<script setup lang="ts">
import { computed, onMounted, ref, type Component } from 'vue';
import { BaseButton } from '@/shared/ui';

/** Button ref */
const buttonRef = ref<Component | Element | null>(null);

/** Button element */
const buttonElement = computed(() => {
  let element = null;

  if (buttonRef.value instanceof Element) {
    element = buttonRef.value;
  }

  return element;
});

// run after mounting
onMounted(() => {
  // displays an interactive element in the console
  console.log(buttonElement);
});
</script>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.