Is it possible in Vue to dynamically use components based on the value of a route parameter?
Here's an example of what I'm trying to do:
path: '/model/:modelName',
components: {
default: ModelDefinition.models.find((model) => model.key === $route.params.modelName),
},
The problem is: $route is not available here. Neither is modelName.
What might be nice if default could also be a function (similar to props):
path: '/model/:modelName',
components: {
default: (modelName) => (ModelDefinition.models.find((model) => model.key === $route.params.modelName)),
},
This function actually gets called to my surprise, but the argument seems to be some sort of function, and certainly not the modelName I'm hoping for.
The reason I need this is: there's actually a lot more configuration for this route, specifying lots of components and values for various things. If I have to specify that for every model, it's going to be long and repetitive, and would have to be expanded for every model we add to our system.
I'm not sure you have to "do magic" with vue-router - why don't you just handle the parameter inside your component (or create a container-component, and handle the parameter there)?
The snippet below shows that you can use external data to control what component should be displayed. If you pass your model parameter down, then any existing (and registered) component-name can be displayed.
const CompA = {
template: `<div class="border-blue">COMP-A</div>`
}
const CompB = {
template: `<div class="border-red">COMP-B</div>`
}
new Vue({
el: "#app",
components: {
CompA,
CompB
},
data: {
selected: ''
}
})
[class^="border-"] {
padding: 10px;
}
.border-blue {
border: 1px solid blue;
}
.border-red {
border: 1px solid red;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<div id="app">
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>CompA</option>
<option>CompB</option>
</select>
<br />
<span>Selected: {{ selected }}</span>
<br />
<br /> Here's a variable component - based on external data:
<component :is="selected"></component>
</div>
In components declaration, this is not available and hence you can not access the props and other instance data.
What you can do is create a computed property that returns a component. Like this:
<template>
<component :is="componentInstance" />
</template>
<script>
export default {
props: {
componentName: String,
},
computed: {
componentInstance() {
return () => import(`./modules/${this.componentName}/template.vue`);
},
},
};
</script>
Related
how do you affect multiple (in this case just 2) children components owned by two different parent components when an action is triggered by one of the children components?
For example I have a component, lets call it <component-one/>. Inside this component I have something like below:
<div #mouseover="hover=true" #mouseleave="hover=false" :class="setColour">
<div class="icon-wrapper commercial-layout position-relative">
<u-button icon color="transparent" #click="toggleCommercials">
<u-icon :icon="icon" color="white"/>
</u-button>
<small class="commercial-ind">COMMERCIAL ADS</small>
<div class="commercial-layout commercial-ind">{{hide}}</div>
</div>
</div>
computed: {
setColour () {
if (this.hover) {
return 'bg-danger'
}
else if (this.commercials) {
return 'bg-primary'
}
else if (!this.commercials) {
return 'bg-secondary'
}
},
watch: {
setColour: function(val) {
console.log("val",val)
}
}
But somewhere else in the code base I have two other components, lets call them <component-two/> and <component-three/>. Inside those components I use component-one. When I push on the button from component-two I want the same effect to also be triggered in component-three, and vice versa, but I'm not quite sure how to achieve that.
Currently both component-two and component-three just have component-one. I've tried adding a watch in component-one but it doesn't really do anything other than capturing changes to the setColour computed property. (I naively thought by capturing the change, all places where component-one is used will get updated)
I'm not sure I totally understand your specific component relationships, but in general I recommend using Vuex.
Using Vue 2 and the CLI, I created sample SFCs that use Vuex to store the background color CSS style. Each child is associated with a specific color, and clicking it's button updates the color of all sibling components.
/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
commonBgColor: 'navajowhite'
},
mutations: {
updateBgColor(state, newColor) {
state.commonBgColor = newColor;
}
}
})
Parent.vue
<template>
<div class="parent">
<child initBgColor="aquamarine" instanceName="One" />
<child initBgColor="mediumorchid" instanceName="Two" />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
}
}
</script>
Child.vue
<template>
<div class="child">
<div class="row">
<div class="col-md-6" :style="currentBgColor">
<span>Sibling Component {{ instanceName }}</span>
<button type="button" class="btn btn-secondary" #click="updateCommonBgColor">Change All Sibling Colors</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
initBgColor: {
type: String,
required: true
},
instanceName: {
type: String,
required: true
}
},
data() {
return {
backgroundColor: this.initBgColor
}
},
computed: {
currentBgColor() {
return 'background-color: ' + this.$store.state.commonBgColor;
}
},
methods: {
updateCommonBgColor() {
this.$store.commit('updateBgColor', this.backgroundColor);
}
}
}
</script>
<style scoped>
.child {
margin-top: 0.5rem;
}
span {
font-size: 1.5rem;
padding: 0.5rem;
}
button {
float: right;
}
</style>
I'm making a todo app in vue.js which has a component TodoItem
<template>
<div class="todo-item" v-bind:class="{'is-completed':todo.completed}">
<p>
<input type="checkbox" #change="markCompleted" />
{{todo.task}}
<button class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props: ["todo"],
methods: {
markCompleted() {
this.todo.completed = true
},
},
};
</script>
todo prop that I'm passing:
{
id:1,
task:'todo 1',
completed:false
}
but it is throwing an error error Unexpected mutation of "todo" prop
Method 1 (Vue 2.3.0+) - From your parent component, you can pass prop with sync modifier
Parent Component
<TodoItem v-for="todo in todoList" :key="todo.id" todo_prop.sync="todo">
Child Component
<template>
<div class="todo-item" v-bind:class="{'is-completed':todo.completed}">
<p>
<input type="checkbox" #change="markCompleted" />
{{todo.task}}
<button class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props: ["todo_prop"],
data() {
return {
todo: this.todo_prop
}
},
methods: {
markCompleted() {
this.todo.completed = true
},
},
};
</script>
Method 2 - Pass props from parent component without sync modifier and emit an event when the value changed. For this method, everything else is similar as well. Just need to emit an event when the todo item changed to completed.
The code is untested. Apologies if anything does not work.
What happen ? : Mutating a prop locally is now considered an anti-pattern, e.g. declaring a prop and then setting this.myProp = 'someOtherValue' in the component. Due to the new rendering mechanism, whenever the parent component re-renders, the child component’s local changes will be overwritten.
Solution : You can storage it as local data.
export default {
name: "TodoItem",
props: ["todo"],
data() {
return {
todoLocal: this.todo,
};
},
methods: {
markComplete() {
this.todoLocal.completed = !this.todoLocal.completed;
},
},
};
For me to fix this problem I store props in todos data im watching brad vue tutorials and i get this error this is my actual codes and its working.
<template>
<div class="todo-item" v-bind:class="{ 'is-complete': todo.completed }">
<p>
<input
type="checkbox"
v-on:change="markComplete(todo.completed)"
v-bind:checked="todo.completed"
/>
{{ todo.title }}
<!-- <button #click="$emit('del-todo', todo.id)" class="del">x</button> -->
</p>
</div>
</template>
<script>
export default {
name: 'TodoItem',
props: ['todo'],
data() {
return {
todos: this.todo,
}
},
methods: {
markComplete(isComplete) {
this.todos.completed = !isComplete
},
},
}
</script>
<style scoped>
.todo-item {
background: #f4f4f4;
padding: 10px;
border-bottom: 1px #ccc dotted;
}
.is-complete {
text-decoration: line-through;
}
.del {
background: #ff0000;
color: #fff;
border: none;
padding: 5px 9px;
border-radius: 50%;
cursor: pointer;
float: right;
}
</style>
One of the core principles of VueJS is that child components never mutate a prop.
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around.
If you wish to have the child component update todo.completed, you have two choices:
Use .sync modifier (Recommended)
This approach will require a bit of change to your props. You can read more about it here.
Parent component
<template>
<div>
...
<todo-item :task="nextTodo.task" :completed.sync="nextTodo.completed"/>
</div>
</template>
Child component
<template>
<div class="todo-item" v-bind:class="{'is-completed':completed}">
<p>
<input type="checkbox" #change="markCompleted" />
{{task}}
<button class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props: ["task", "completed"],
methods: {
markCompleted() {
this.$emit('update:completed', true)
},
},
};
</script>
Use a custom event
Vue allows you set up listeners in your parent for events that the child will emit. Your child component can use this mechanism to ask the parent to change things. In fact, the above .sync modifier is doing exactly this behind the scenes.
Parent component
<template>
<div>
...
<todo-item :todo="nextTodo" #set-completed="$value => { nextTodo.completed = $value }/>
</div>
</template>
Child component
<template>
<div class="todo-item" v-bind:class="{'is-completed':todo.completed}">
<p>
<input type="checkbox" #change="markCompleted" />
{{todo.task}}
<button class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props: ["todo"],
methods: {
markCompleted() {
this.$emit('set-completed', true)
},
},
};
</script>
You can't change a prop from inside a component - they are meant to be set by the parent only. It's a one-directional communication path.
You can try one of two things - either move your logic for detecting a todo has been completed to the parent, or feed the prop into a new variable in the data() lifecycle hook (this will only happen when the component is loaded for the first time, so you won't be able to update from outside the component, if that's important for your use case).
The canonical way to achieve n-deep prop binding in Vue 3 is to wrap your prop with a simple computed property. This is an example of a component that will communicate changes to it's selected property to it's parent--who is ultimately responsible for storing the state.
<template>
<!-- In this example my-child-component also has a "selected" prop -->
<my-child-component v-model:selected="syncSelectedId" />
</template>
<script lang="ts">
export default defineComponent({
components: { MyChildComponent },
props: {
selected: {
type: String,
required: true,
}
},
emits: ['update:selected'],
setup(props, context) {
const syncSelectedId = computed<string>({
get() {
return props.selected;
},
set(newVal: string) {
context.emit('update:selected', newVal);
},
});
return {
syncSelectedId,
}
}
});
So to re-iterate: With this strategy the highest level parent is the holder of the state. The code above assumes that there is a parent component in the hierarchy (so this component is just a "middle-man").
Then my-child-component can simply emit its own update:selected event to cause the state to change. That child will be updated appropriately through it's prop after the emit event causes the parent chain to propagate that change up (through emits) and then back down the component hierarchy (through props).
If you wanted to you could modify the code above to make it the "owner" of the state:
<template>
<my-child-component v-model:selected="selected" />
</template>
<script lang="ts">
export default defineComponent({
components: { MyChildComponent },
setup(props, context) {
const selected = ref('');
return {
selected,
}
}
});
And now of course you won't run into the "Unexpected mutation of X prop" error.
Another option is to have a prop that serves as a "default value" for a given state:
<template>
<my-child-component v-model:selected="selected" />
</template>
<script lang="ts">
export default defineComponent({
components: { MyChildComponent },
props: {
defaultSelected: {
type: String,
required: false,
default: ''
}
},
setup(props, context) {
const selected = ref(props.defaultSelected);
return {
selected,
}
}
});
And in this code above keep in mind that selected will NOT change if defaultSelected changes after the component has been initialized.
And lastly it's worth noting that you could write more sophisticated code to detect if a property is supplied--and if not use an internal state variable to store the value. I use this pattern for re-usable components that could be embedded in places where the parent wants to control the state OR in places where the parent is happy to delegate the storage of the state to the child:
<template>
<!-- In this example my-child-component also has a "selected" prop -->
<my-child-component v-model:selected="syncSelectedId" />
</template>
<script lang="ts">
export default defineComponent({
components: { MyChildComponent },
props: {
selected: {
type: String,
required: false,
default: null // Important: parent MUST pass non-null value if it wants to control state
}
},
emits: ['update:selected'],
setup(props, context) {
// This is state storage used if prop.selected is not provided
const _selected = ref('');
const syncSelectedId = computed<string>({
get() {
return props.selected === null ? _selected.value : props.selected;
},
set(newVal: string) {
if (props.selected !== null) {
// Using prop.selected as the driving model...
if (newVal !== props.selected) {
// We need to set to empty string (never null)
context.emit('update:selectedId', (newVal == null ? '' : newVal));
}
} else { // Storing selection state with _selectedId
if (newVal !== _selected.value) {
_selected.value = newVal == null ? '' : newVal;
context.emit('update:selected', _selected);
}
}
},
});
return {
syncSelectedId,
}
}
});
This last example is tricky... it gives special meaning to null and requires that you be very mindful of potential values of your state. In my example empty string is my representation for "no selection" and null is used as a flag for "no parent model of this state".
Mainly, property mutation is now deprecated and parent properties are overwritten when the parent component renders its DOM.
Here's the official documentation about it. We can still achieve this in multiple possible ways. Through a data property, a computed property, and component events.
When we want to pass this value back to the parent component as well as the nested child component of the current child component, using a data property would be useful as shown in the following example.
Example:
Calling your child component from the parent component like this.
Parent component:
<template>
<TodoItem :todoParent="todo" />
</template>
<script>
export default {
data() {
return {
todo: {
id:1,
task:'todo 1',
completed:false
}
};
}
}
</script>
Child component:
<template>
<div class="todo-item" v-bind:class="{'is-completed':todo.completed}">
<p>
<input type="checkbox" #change="markCompleted" />
{{todo.task}}
<button class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props: ["todoParent"],
data() {
return {
todo: this.todoParent,
};
},
methods: {
markCompleted() {
this.todo.completed = true
},
},
};
</script>
Even you can pass this property to the nested child component and it won't give this error/warning.
Other use cases when you only need this property sync between parent and child component. It can be achieved using the sync modifier from Vue. v-model can also be useful. Many other examples are available in this question thread.
Example2: using component events.
We can emit the event from the child component as below.
Parent component:
<template>
<TodoItem :todo="todo" #markCompletedParent="markCompleted" />
</template>
<script>
export default {
data() {
return {
todo: {
id:1,
task:'todo 1',
completed:false
}
};
},
methods: {
markCompleted() {
this.todo.completed = true
},
}
}
</script>
Child component:
<template>
<div class="todo-item" v-bind:class="{'is-completed':todo.completed}">
<p>
<input type="checkbox" #change="markCompleted" />
{{todo.task}}
<button class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props: ["todo"],
methods: {
markCompleted() {
this.$emit('markCompletedParent', true)
},
}
};
</script>
While you can still custom-bind events to handle this, .sync property extensions are considered deprecated. In Vue3 (at least) you can and usually should use the v-model:property declaration, similar to how you bind the property to the actual input. You just need to bind the inner input with :value and have it emit a matching update:property
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
/>
</template>
And use thusly:
<CustomInput v-model="searchText" />
I'm using vue-i18n to translate messages in my vue app. I have some global translations that are added in new VueI18n(...) as well as some component based translations in a component named c-parent. The component contains child components named c-child. Now, I would like to use the component based translations of c-parent also in c-child.
I made a small example in this fiddle: https://jsfiddle.net/d80o7mpL/
The problem is in the last line of the output: The message in c-child is not translated using the component based translations of c-parent.
Since global translations are "inherited" by all components, I would expect the same for component based translations (in their respective component subtree). Is there a way to achieve this in vue-i18n?
Well, you need to pass the text to child component using props.
Global translations are "inherited" by all components. But you're using local translation in child.
const globalMessages = {
en: { global: { title: 'Vue i18n: usage of component based translations' } }
}
const componentLocalMessages = {
en: { local: {
title: "I\'m a translated title",
text: "I\'m a translated text"
}}
}
Vue.component('c-parent', {
i18n: {
messages: componentLocalMessages
},
template: `
<div>
<div>c-parent component based translation: {{ $t('local.title') }}</div>
<c-child :text="$t('local.title')"></c-child>
</div>
`
})
Vue.component('c-child', {
props: ['text'],
template: `
<div>c-child translation: {{ text }}</div>
`
})
Vue.component('app', {
template: '<c-parent />'
})
const i18n = new VueI18n({
locale: 'en',
messages: globalMessages
})
new Vue({
i18n,
el: "#app",
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
}
h5 {
margin: 1em 0 .5em 0;
}
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vue-i18n"></script>
<div id="app">
<h2>{{ $t('global.title') }}</h2>
We define two Vue components: <code><c-child/></code> contained in <code><c-parent/></code>.
<code><c-parent/></code> defines some component based translations. We would like to use the
parent's translations in the child but it does not work.
<h5>Example:</h5>
<app />
</div>
What I'm doing is using i18n.mergeLocaleMessage in router.ts to merge a particular .i18n.json translation file (by setting a meta.i18n property) for each route:
const router = new Router({
[...]
{
path: '/settings',
name: 'settings',
component: () => import('./views/Settings.vue'),
meta: {
i18n: require('./views/Settings.i18n.json'),
},
},
[...]
});
router.beforeEach((to, from, next) => {
// load view-scoped translations?
if (!!to.meta.i18n) {
Object.keys(to.meta.i18n).forEach((lang) => i18n.mergeLocaleMessage(lang, to.meta.i18n[lang]));
}
next();
});
With Settings.i18n.json being like:
{
"en":
{
"Key": "Key"
},
"es":
{
"Key": "Clave"
}
}
That way, all child components will use the same translation file.
In case you can't use vue-router, maybe you can do it in the parent component's mounted() hook (haven't tried that)
I had the same situation with i18n.
Let's say we have a "card" object prop which it includes the needed language ( was my case) that we'll use in a CardModal.vue component which will be the parent.
So what i did was get the needed locale json file ( based on the prop language) and adding those messages within the card prop.
So in the parent component we'll have:
<template>
<div id="card-modal">
<h1> {{ card.locales.title }} </h1>
<ChildComponent card="card" />
</div>
</template>
<script>
export default {
name: 'CardModal',
props: {
card: {
type: Object,
required: true,
}
}
data() {
return {
locale: this.card.language, //'en' or 'es'
i18n: {
en: require('#/locales/en'),
es: require('#/locales/es'),
},
}
},
created() {
this.card.locales = this.i18n[this.locale].card_modal
}
}
</script>
Notice that we are not relying in the plugin function anymore ( $t() ) and we are only changing the locale in the current component. I did it in this way cause i didn't want to use the "i18n" tag in each child component and wanted to keep all the locales messages in one single json file per language. I was already using the card prop in all child components so that's why i added the locales to that object.
If you need a way to change the locale using a select tag in the component, we can use a watcher for the locale data property like the docs shows
I would like to create a vue element from a string from my database.
In this case, it should be a message with a smiley emoji.
I actually save it like: Some text with Emoji: :santa::skin-tone-3:, and replace all valid string between '::' with the <Emoji emoji=':santa::skin-tone-3:' :size='16' />
<template>
<span class=message v-html=convertedMessage></div>
</template>
<script>
import { Emoji } from 'emoji-mart-vue'
export default {
components: {
Emoji
},
computed:{
convertedMessage(){
return "Some text with Emoji: "+"<Emoji emoji=':santa::skin-tone-3:' :size='16' />"
}
}
}
</script>
But instead of the rendered element which should be something like:
<span data-v-7f853594="" style="display: inline-block; width: 32px; height: 32px; background-image: url("https://unpkg.com/emoji-datasource-apple#4.0.4/img/apple/sheets/64.png"); background-size: 5200%; background-position: 15.6863% 41.1765%;"></span>
I only get:
<emoji emoji=":santa::skin-tone-3:" :size="16"></emoji>
What is the best possibility to render this Element like intended?
Here are some much easier ways to do what you generally want. If you give more specifics, your right direction may be a strategy pattern before one of these solutions, but one of these solutions is probably what you want:
1) Vue lets you dynamically define components right out of the box, so this single line:
<component v-for="(component, index) in components" :key="'component'+index" :is="component.name" v-bind="component.props" />
...would draw a bunch of components in an array of objects like this (for example): {name: 'myComponentName', props: {foo: 1, bar: 'baz'}}.
2) Vue lets you inject HTML into components by simply adding v-html="variable"
For example, here is a component that creates dynamic SVG icons, where the contents of the SVG is dynamically injected from JavaScript variables...
<template>
<svg xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 18 18"
:aria-labelledby="name"
role="presentation"
>
<title :id="name" lang="en">{{name}} icon</title>
<g :fill="color" v-html="path">
</g>
</svg>
</template>
<script>
import icons from '../common/icons'
export default {
props: {
name: {
type: String,
default: 'box'
},
width: {
type: [Number, String],
default: 18
},
height: {
type: [Number, String],
default: 18
},
color: {
type: String,
default: 'currentColor'
}
},
data () {
return {
path: icons[this.name]
}
},
created () {
console.log(icons)
}
}
</script>
<style scoped>
svg {
display: inline-block;
vertical-align: baseline;
margin-bottom: -2px;
}
</style>
3) Vue lets you dynamically define your component template through this.$options.template:
export default {
props: ['name', 'props'],
template: '',
created(){
this.$options.template = `<component :is="name" ${props.join(' ')} ></component>`
},
}
4) Vue lets you define a render function, so proxy components or other advanced shenanigans are trivial:
Vue.component('component-proxy', {
props: {
name: {
type: String,
required: true
},
props: {
type: Object,
default: () => {}
}
},
render(h) {
// Note the h function can render anything, like h('div') works too.
// the JS object that follows can contain anything like on, class, or more elements
return h(this.name, {
attrs: this.props
});
}
});
A smart genius wrote a jsbin for this here: http://jsbin.com/fifatod/5/edit?html,js,output
5) Vue allows you to create components with Vue.extend or even passing in raw JavaScript objects into a page or apps components section, like this, which creates a component named "foo" from a simple string for the template and an array for props, you could also extend the data, created, on, etc. the same way using the JS object alone:
new Vue({
el: '#app',
data: {
foo: 'bar',
props: {a: 'a', b: 'b'}
},
components: {
foo: {
template: '<p>{{ a }} {{ b }}</p>',
props: ['a', 'b']
}
}
})
What i figured out now:
convertedMessage(){
let el = Vue.compile("<Emoji emoji=':santa::skin-tone-3:' :size='16' />")
el = new Vue({
components: {
Emoji
},
render: el.render,
staticRenderFns: el.staticRenderFns
}).$mount()
return "Some text with Emoji: "+el.$el.innerHTML
}
Maybe there is still a better solution to handle this?
Here's how I went about when I needed to do something similar.
I rendered the component, say, <my-component> normally, but since I only needed rendered HTML, I wrapped it inside a <div class="hidden"> like so:
<div class="hidden">
<my-component />
</div>
With CSS:
.hidden {
display: none;
}
This way, I can refer to the element through $refs or you could get the element from the DOM using document.querySelector() while keeping it invisible to the end users.
So in the above example, to get the rendered HTML, You'd only need to do this:
let el = document.querySelector('.hidden');
let renderedHTMLString = el.children[0].outerHTML;
This way, you get the rendered HTML, without any overhead costs that's associated with Vue.compile or any other plugin. Render it normally. Hide it. Access it's outerHTML.
v-html only render plain HTML, see https://v2.vuejs.org/v2/guide/syntax.html#Raw-HTML
In your case you should probably take a look at render functions and JSX. I'm not an expert but it seems that some people are acheiving what you want with the dangerouslySetInnerHTML JSX function. Take a look at this thread : How do I convert a string to jsx?
I know sometimes we have no choice but if you can I think the best solution could be to avoid generating the template from the backend as it breaks separation of concern (and also, probably, security issues).
I'm new using Vue.js and I had a difficulty creating a Button component.
How can I program this component to conditional rendering? In other words, maybe it should be rendering as a router-link maybe as a button? Like that:
<Button type="button" #click="alert('hi!')">It's a button.</Button>
// -> Should return as a <button>.
<Button :to="{ name: 'SomeRoute' }">It's a link.</Button>
// -> Should return as a <router-link>.
You can toggle the tag inside render() or just use <component>.
According to the official specification for Dynamic Components:
You can use the same mount point and dynamically switch between multiple components using the reserved <component> element and dynamically bind to it's is attribute.
Here's an example for your case:
ButtonControl.vue
<template>
<component :is="type" :to="to">
{{ value }}
</component>
</template>
<script>
export default {
computed: {
type () {
if (this.to) {
return 'router-link'
}
return 'button'
}
},
props: {
to: {
required: false
},
value: {
type: String
}
}
}
</script>
Now you can easily use it for a button:
<button-control value="Something"></button-control>
Or a router-link:
<button-control to="/" value="Something"></button-control>
This is an excellent behavior to keep in mind when it's necessary to create elements that may have links or not, such as buttons or cards.
You can create a custom component which can dynamically render as a different tag using the v-if, v-else-if and v-else directives. As long as Vue can tell that the custom component will have a single root element after it has been rendered, it won't complain.
But first off, you shouldn't name a custom component using the name of "built-in or reserved HTML elements", as the Vue warning you'll get will tell you.
It doesn't make sense to me why you want a single component to conditionally render as a <button> or a <router-link> (which itself renders to an <a> element by default). But if you really want to do that, here's an example:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
Vue.component('linkOrButton', {
template: `
<router-link v-if="type === 'link'" :to="to">I'm a router-link</router-link>
<button v-else-if="type ==='button'">I'm a button</button>
<div v-else>I'm a just a div</div>
`,
props: ['type', 'to']
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<link-or-button type="link" to="/"></link-or-button>
<link-or-button type="button"></link-or-button>
<link-or-button></link-or-button>
</div>
If you're just trying to render a <router-link> as a <button> instead of an <a>, then you can specify that via the tag prop on the <router-link> itself:
Vue.use(VueRouter);
const router = new VueRouter({
routes: [ { path: '/' } ]
})
new Vue({ el: '#app', router })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.1/vue-router.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.js"></script>
<div id="app">
<router-link to="/">I'm an a</router-link>
<router-link to="/" tag="button">I'm a button</router-link>
</div>
You can achieve that through render functions.
render: function (h) {
if(this.to){ // i am not sure if presence of to props is your condition
return h(routerLink, { props: { to: this.to } },this.$slots.default)
}
return h('a', this.$slots.default)
}
That should help you start into the right direction
I don't think you'd be able to render a <router-link> or <button> conditionally without having a parent element.
What you can do is decide what to do on click as well as style your element based on the props passed.
template: `<a :class="{btn: !isLink, link: isLink}" #click="handleClick"><slot>Default content</slot></a>`,
props: ['to'],
computed: {
isLink () { return !!this.to }
},
methods: {
handleClick () {
if (this.isLink) {
this.$router.push(this.to)
}
this.$emit('click') // edited this to always emit
}
}
I would follow the advice by #Phil and use v-if but if you'd rather use one component, you can programmatically navigate in your click method.
Your code can look something like this:
<template>
<Button type="button" #click="handleLink">It's a button.</Button>
</template>
<script>
export default {
name: 'my-button',
props: {
routerLink: {
type: Boolean,
default: false
}
},
methods: {
handleLink () {
if (this.routerLink) {
this.$router.push({ name: 'SomeRoute' })
} else {
alert("hi!")
}
}
}
}
</script>