I have a component where I have a few blocks that repeat throughout the template.
These blocks may have a conditional or two, and might call some methods in event handlers, but mostly they're pretty simple.
It is really not worth creating an entire, separate component for a few elements, plus passing around data and methods isn't exactly trivial - and it makes the component more difficult to maintain. These blocks won't be used in any other components.
I really need to be able to define a "subcomponent" or "template" inside this component for these blocks.
(I don't think this is possible yet, but that's why you're here)
Has anyone figured out a clean solution for this?
Components can be defined as render functions, and this is especially easy with JSX:
const ComponentA = props => (
<div>
<label>{props.label} <input type="text" /></label>
</div>
)
You could declare multiple subcomponents as render functions (or full component definition objects if needed) in the same file, and reuse them in the component's template. Vue 3's <script setup> block also helps to make this more concise:
<script lang="jsx" setup>
const SubComponentA = (props) => (
<div>
<label>{props.label}</label>
<input type="number" />
</div>
)
const SubComponentB = (props) => (
<div>
<label>{props.label}</label>
<textarea />
</div>
)
</script>
<template>
<SubComponentA label="Label 1" />
<SubComponentB label="Label 2" />
<SubComponentA label="Label 3" />
<SubComponentB label="Label 4" />
</template>
demo
Related
The Problem
Let's say we have a page template written as a Web component in a shared library to keep the company design system consistent. That page has some slots:
export class PageTemplate extends LitElement {
static properties = {
title: { type: String },
};
render() {
return html`
<div>
<h1>${title}</h1>
<slot name="template-body"></slot>
<div class="some-special-styles">
<slot name="template-buttons"></slot>
</div>
</div>
`;
}
}
customElements.define("page-template", PageTemplate);
Then we use this template in a Vue (v3.2.45) application on a base component to be used in the same app by multiple pages.
//page-base.vue
<template>
<page-template title="My App Name">
<slot name="base-body"></slot>
<slot name="base-buttons"></slot>
</page-template>
</template>
Here, we will use the page base vue component on a specific page.
//login-page.vue
<template>
<PageBase>
<template #base-body>
<div slot="template-body">
<input placeholder="some special code"/>
</div>
</template>
<template #base-buttons>
<button slot="template-buttons">login</button>
<button slot="template-buttons">back</button>
</template>
</PageBase>
</template>
To make the login page components show inside that original page template web component; we need to declare the slot property on the leaf components like in <button slot="template-buttons">
How can I implement the Vue Page Base component to avoid the need to remember to set the slot property in every leaf vue component?
Things I've Tryied
I've tried to solve this using the vanilla web syntax below, but Vue appears not to dispatch that information to the final HTML:
//page-base.vue
<template>
<page-template title="My App Name">
<!-- this does not work -->
<slot name="base-body" slot="template-body"></slot>
<slot name="base-buttons" slot="template-buttons"></slot>
</page-template>
</template>
There was also an attempt (after a suggestion in the comments) to use a template as a ghost intermediate in the page base. But nothing was rendered at runtime.
//page-base.vue
<template>
<page-template title="My App Name">
<!-- i can't have that span because of some-special-styles applied in the template-->
<template slot="template-body"><slot name="base-body"></slot></template>
<template slot="template-buttons"><slot name="base-buttons"></slot></template>
</page-template>
</template>
The approach to using some middle element to make the connection (like below) enables content rendering. Still, it does not work for the project requirements because, for style reasons, I need that the final components be the top-most nodes in the page template slots.
//page-base.vue
<template>
<page-template title="My App Name">
<!-- although it runs, i can't have these spans because of some-special-styles applied in the template -->
<span slot="template-body"><slot name="base-body"></slot></span>
<span slot="template-buttons"><slot name="base-buttons"></slot></span>
</page-template>
</template>
Let's say I want to create my own Link.
const Link = ({ href, style }) => {
return <a href={href} class={style}>
<Slot />
</a>
}
Now I want to use this Link in the Menu of my website, and the Menu component is imported inside the main layout.
// main layout
<Menu />
<Slot />
<Footer />
Here, I get this error:
[vite] Internal server error: can not be rendered because one of its ancestor is already a .
This goes against the HTML spec: https://html.spec.whatwg.org/multipage/dom.html#interactive-content
Why does this happen? It's because inside the main layout, we practically included another <Slot /> by including <Menu /> which contains <Link /> components.
So, what do you think we should do here?
If we ask all developers to specify the name of the slot, that's highly inefficient and dirty:
<Link href="/">
<span q:slot='link'>About us</span>
</Link>
This is very ugly and inefficient. I don't have many slots in my Link component. I have one Slot. I should not be specifying a name for it.
What should I do?
I have input on custom component and when i click on the next button on the wrapper component i need to emit details to the parent component.
How is this possible in vue?
wrapper.vue
<template>
<div :id="contentId" class="mt-3">
<div>
<slot></slot>
</div>
<b-row class="float-right">
<b-col>
<button cssClass="e-outline" v-on:click="btnNext">{{nextButtonText}}</button>
</b-col>
</b-row>
</div>
</template>
parent.vue
<template>
<div>
<Wrapper contentId="1">
<CustomComponent1 />
</wrapper>
<Wrapper contentId="2">
<CustomComponent1 />
</wrapper>
</div>
</template>
customComponent1.vue
<template>
<div>
<input v-model="name" />
<input v-model="name2" />
</div>
</template>
code above is for illustrative purposes.
The problem is that the wrapper doesn't innately have access to data of the scoped component, therefore these links have to be created manually. There is no way to tell how many children or slots the component may have, so this kind of functionality is not part of the vue magic.
So in an example where you have parent App component, which holds a Wrapper that has a MyInput component in the slot...
MyInput
The MyInout component doesn't automatically update other components, so it needs to be setup to $emit the internal data.
This can be done using a watch, #change listener for the input, or some other way. You can emit multiple datum as they change, or use a single payload with all the data
this.$emit("input", myData);
App
The App needs to explicitly connect the data between MyInout and Wrapper
<Wrapper> <MyInput #input="onInput" slot-scope="{ onInput }" /> </Wrapper>
The magic/trick happens here, where we bind the input emit function of the input to the onInput function using slot-scope.
Wrapper
The wrapper then needs to listen to the events passed (via App) from Wrapper
<slot :onInput="onInput" />
where onInput is a method that would process the data
see example below
I would recommend the following reading
https://github.com/vuejs/vue/issues/4332 (specifically Evan's response why it's not possible)
https://adamwathan.me/renderless-components-in-vuejs/ Adam has a thoroughly documented way of using render functions and slots to abstract functionality from the UI. While it's not directly related, it's a worthwhile read and may provide more info on using slot-scope as well as some perspective on improving the structure of UI components.
I am design an Vuejs app which page render based on route.
e.g. for
route = /, Component = Main.vue
<template>
<div>
<toolbar :user="user"></toolbar>
<app-content></app-content>
</div>
</template>
route = /:user, Component = User.vue
<template>
<div>
<toolbar :user="user"></toolbar>
<userHeader></userHeader>
<app-content></app-content>
</div>
</template>
When the page is show, the toolbar component will fetch data from server, the problem is, when the page go from / to /user, the data fetching data X 2 because that are 2 toolbar components in the app itself.
How should resolve this issue ? is that any way to reuse share component instances like toolbar ?
or should i put the design in one whole component instead ? ( use v-if to show hide the additional component)
You should be having <toolbar /> outside of <router-view></router-view>.
So your code should look like:
<div id="app">
<toolbar user="user" />
<router-view />
</div>
With this <toolbar /> won't change even if you change your routes, and will result in data fetching for a single time only.
I have a component inside a .vue file that can benefit from reusing a chunk of code. I know I can move that code to a separate .vue file and import it as a new component. However, this component would not be used anywhere else and I'd like to avoid cluttering the directory. Is it possible to declare this component's template inside the parent without using the in-code template:"<div>.....</div>" stuff?
This is the idea:
<template>
<div>
...some html here...
<div v-for="item in items">
{{item.name}}:
<div v-if="item.available">YES!</div>
<div v-else>NO :(</div>
</div>
...some other components and data here...
<div v-for="item in items">
{{item.name}}:
<div v-if="item.available">YES!</div>
<div v-else>NO :(</div>
</div>
</div>
</template>
I would like to be able to do something like this:
<template>
<div>
...some html here...
<div v-for="item in items">
<itemizer inline-template v-model="item">
{{value.name}}:
<div v-if="value.available">YES!</div>
<div v-else>NO :(</div>
</itemizer>
</div>
...some other components and data here...
<div v-for="item in items">
<itemizer v-model="item"/>
</div>
</div>
</template>
However, from what I understand this is not possible.
Unfortunately this pattern does not appear to be supported by the creator of Vue:
I personally feel the syntax is less maintainable than [Single File Components]
Note that we want to keep the SFC syntax as consistent possible, because it involves a big ecosystem of tools that need to support any new features added (e.g. Vetur would need to do something very different for handling SFCs with multiple scripts/templates). The proposed syntax, IMO, does not justify adding the new syntax.
https://github.com/vuejs/vue/pull/7264#issuecomment-352452213
That's too bad, as even beyond flexibility and developer choice, there is a good argument for inlining small functions that are not used by other components in order to reduce complexity. It's a common pattern in React and does not inhibit Single File Components when they're needed. In fact it allows gradual migration as inline components grow.
Here's one of the only resources currently that offers some potential workarounds:
https://codewithhugo.com/writing-multiple-vue-components-in-a-single-file/
I've tried them all and am not satisfied with any of them at this time. At best you can set runtimerCompiler: true and use template strings, but it'll add 10KB to your bundle and you'll likely miss out on full syntax highlighting available in the <template> element. Maybe you can hack Teleport, but I have not made a dedicated attempt.
Actually, this should work. Just register your Vue inline-template like this in the section of your parent .vue file:
<template>
<div v-for="item in items">
<test-template :item="item">
<h1>{{item.text}}</h1>
</test-template>
</div>
</template>
<script>
Vue.component('test-template', {
template:'#hello-world-template',
props: {
item: Object
}
});
export default {...}
</script>
In your parent HTML file, put this:
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
With vue3 there are multiple options:
with vue-jsx you can just declare a component in your script setup section and use that
const Named = defineComponent(() => {
const count = ref(0)
const inc = () => count.value++
return () => (
<button class="named" onClick={inc}>
named {count.value}
</button>
)
})
There is another option described by Michael Thiessen here
Also you can have multiple render function components in one file:
https://staging.vuejs.org/guide/extras/render-function.html
Although it is not supported in Vue core yet, there is a way to use this through vue macros project. See discussion here