So I am making a simple stepper. I have my structure like so:
<stepper :steps="steps">
<template slot="stepper:step">
<div class="">
{{ first }}
</div>
</template>
<template slot="stepper:step">
<div class="">
{{ second }}
</div>
</template>
</stepper>
export default {
data: () => ({
steps: [
{ name: 'First step' },
{ name: 'second step' },
],
first: 'First content',
second 'second content',
})
}
My stepper is like so: https://gist.github.com/natecorkish/81e22edb9e41348e579b705c3a4d3e54
and my step is like so: https://gist.github.com/natecorkish/45108d046ddc7d116110c973b608a774
Now, I am get an errror when rendering the content:
vue.runtime.esm.js?2b0e:1888 TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property '_renderProxy' closes the circle
at JSON.stringify ()
I have tried VueJS Render VNode but that only renders a couple of my divs/inputs. So, judging from my code, how can I fix this?
Related
I currently have a v-for which creates a list of components depending on an array of name values.
Depending on the step === 'name' check, I'm displaying the relevant component. This method requires that I list every single possible component with a v-if and I think there should be a cleaner way to accomplish this.
<div v-for="(step, idx) in steps" :key="idx">
<div v-if="step === 'url'">
<Goto #removeStep="removeStep(idx)" />
</div>
<div v-if="step === 'click'">
<Click #removeStep="removeStep(idx)" />
</div>
<div v-if="step === 'search'">
<Search #removeStep="removeStep(idx)" />
</div>
...
</div>
I'd rather interpolate the component directly in my for loop.
For example, something like...
<div v-for="(step, idx) in steps" :key="idx">
<[step.component] #removeStep="removeStep(idx)" :title="step.title" />
</div>
My array of dynamic components would then look like:
steps: [
{
component: 'Goto',
title: 'Go to Url'
},
{
component: 'Click',
title: 'Click Something'
}
]
I do not want to have all component options in a single file component, and prefer them to remain as separate components as I have currently.
Could I accomplish the above using either some form of interpolation or a computed prop perhaps?
Vue supports dynamic components with <component is="COMPONENT_NAME">. You could register your components locally, and then bind <component>.is to the component names in your steps array:
<template>
<div>
<component
:is="step.component"
v-for="(step, idx) in steps"
:key="step.component"
:title="step.title"
#remove-step="removeStep(idx)"
/>
</div>
</template>
<script>
import Goto from './components/Goto'
import Click from './components/Click'
import Search from './components/Search'
export default {
components: {
Goto,
Click,
Search,
},
data() {
return {
steps: [
{
component: 'Goto',
title: 'Go to Url',
},
{
component: 'Click',
title: 'Click Something',
},
{
component: 'Search',
title: 'Search the Internet',
},
],
}
},
methods: {
removeStep(idx) {
console.log('remove step', idx)
},
},
}
</script>
demo
What I want to achieve is something like:
<li v-for="(item, index) in items" :key="index>
<div v-if="item.Component">
<item.Component :value="item.value" />
</div>
<div v-else>{{ item.value }}</div>
</li>
But anyway I don't like at all this solution. The idea of defining Component key for an item in items list is hard to maintain since at least it is hard to write it in template-style way (usually we are talking about too long HTML inside). Also I don't like to wrap item.Component inside div.
data() {
return {
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `123 {{ value }} 312`
}
}]
};
}
Does anyone know the best-practice solution for this and where Vue describes such case in their docs?
You can use Vue's <component/> tag to dynamically set your component in your list.
<li v-for="(item, index) in items" :key="index>
<component v-if="item.Component" :is="item.Component" :value="item.value"></component>
<div v-else>{{ item.value }}</div>
</li>
<script>
...,
data: () => ({
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `<div>123 {{ value }} 312</div>` // must be enclosed in a element.
}
}]
})
</script>
You can also import a component too so you can create a new file and put your templates and scripts there.
Parent.vue
<script>
import SomeComponent from "#/components/SomeComponent.vue"; //import your component here.
export default {
data() {
return {
list: [
{
value: "abc",
Component: SomeComponent // define your imported component here.
},
]
};
}
};
</script>
SomeComponent.vue
<template>
<div>123 {{ value }} 312</div>
</template>
<script>
export default {
name: "SomeComponent",
props: ["value"]
};
</script>
Here's a demo.
I have this code in my VUE file:
<template>
<div class="row">
<div class="col-12">
<section class="list">
<draggable class="drag-area" :list="picsNew" :options="{animation:200, group:'status'}" :element="'article'" #add="onAdd($event, false)" #change="update">
<article class="card" v-for="(photo, index) in picsNew" :key="photo.id" :data-id="photo.id">
<header>
{{ this.galCode }}{{ photo.filename }}
</header>
</article>
</draggable>
</section>
</div>
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: {
draggable
},
props: ['myPics', 'galId', 'phCode', 'galCode'],
data() {
return {
picsNew: this.myPics,
}
},
methods: {
update() {
this.picsNew.map((photo, index) => {
photo.order = index + 1;
});
let photos = this.picsNew;
console.log(this.galCode)
axios.put('/gallery/' + this.galId + '/updateAll', {
photos: photos
}).then((response) => {
console.log(response.data);
}).catch((error) => {
console.log(error);
})
}
}
}
</script>
in the template, photo.filename works, but this.galCode throws these two errors:
app.js:44152 [Vue warn]: Error in render: "TypeError: Cannot read property 'galCode' of undefined"
found in
---> <DraggablePic> at resources/js/components/draggablepic.vue
app.js:45415 TypeError: Cannot read property 'galCode' of undefined
The variable contains a value, as i am printing it to console. What am I doing wrong?
The problem is that you try to access props variable directly from your template. To solve this, initalize a state variable (under data {}) Within props value as default like you did With picsNeW.
another remark, avoid to use "this" from template, you should access directly datas form its name. Use default value in your props, this is recommended :)
The line {{ this.galCode }}{{ photo.filename }} should be {{ galCode }}{{ photo.filename }}. Your problem is the this you added
First you should define Default value as :
props: {
name: {
type: String,
default: 'default'
},
...
galCode: {
type: Number,
default: 0
},
},
Second be sure you receive desired data, specially in nested object you should check the object defined to prevent receive undefined property error, like use v-if :
<header>
<template v-if="galCode">{{ galCode }}</template> // can use v-else here too print default value
<template v-if="photo.filename"> {{ photo.filename }}</template>
</header>
I'm having a difficult time understanding slots for some reason and why they should even be used. The only reason I can think of that would be nice for usuage is if we can reference specific properties within a v-for loop of an element and output different templates quicker perhaps...
So, am thinking, and possibly I could be wrong in thinking this, but if I have a variable like so:
<script>
const items: [
{
label: 'My Label',
url: '#',
headerTitle: 'My Header Title'
},
{
label: 'My Label 2',
url: 'https://www.myurl.com',
headerTitle: 'My Header Title 2'
},
{
label: 'My Label 3',
url: 'https://www.myurl3.com'
}
]
export default {
data () {
return {
items: items
}
}
}
</script>
And than in the template, possibly this:
<template>
<div v-for="(item, index) in items" :key="item.id">
<template slot-scope="headerTitle">
<h1>{{ item.headerTitle }}</h1>
</template>
<template slot-scope="label">
<div class="mylabel">
{{ item.label }}
</div>
</template>
<template slot-scope="url">
<a :href="item.url">{{ item.label }}</a>
</template>
</div>
</template>
I don't know if this makes sense or not, but basically using the property as a slot-scope and than for everytime that property is defined, it will output something. But this doesn't work properly. Is this not what slot-scopes are for within component v-for loops? Is this not how to use these properties of an array of objects?
This kinda makes sense to me. Anyways to do it like this perhaps?
Hopefully this is a rather simple question / answer, but I can't find much info in the docs.
Is there a way to enable or disable the anchor generated by <router-link> dependent on whether a prop is passed in or not?
<router-link class="Card__link" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If there's no id passed to this component, I'd like to disable any link being generated.
Is there a way to do this without doubling up the content into a v-if?
Thanks!
Assuming you want to disable anchor tag as in not clickable and look disabled the option is using CSS. isActive should return true by checking prop id.
<router-link class="Card__link" v-bind:class="{ disabled: isActive }" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
<style>
.disabled {
pointer-events:none;
opacity:0.6;
}
<style>
If you want to just disable the navigation , you can use a route guard.
beforeEnter: (to, from, next) => {
next(false);
}
If you need to use it often, consider this:
Create new component
<template>
<router-link
v-if="!disabled"
v-bind="$attrs"
>
<slot/>
</router-link>
<span
v-else
v-bind="$attrs"
>
<slot/>
</span>
</template>
<script>
export default {
name: 'optional-router-link',
props: {
params: Object,
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
Optional, register globally:
Vue.component('optional-router-link', OptionalRouterLink);
Use it as follows:
<optional-router-link
:disabled="isDisabled"
:to="whatever"
>
My link
</optional-router-link>
The problem is that router-link renders as an html anchor tag, and anchor tags do not support the disabled attribute. However you can add tag="button" to router-link:
<router-link :to="myLink" tag="button" :disabled="isDisabled" >
Vue will then render your link as a button, which naturally supports the disabled attribute. Problem solved! The downside is that you have to provide additional styling to make it look like a link. However this is the best way to achieve this functionality and does not rely on any pointer-events hack.
I sometimes do stuff like this:
<component
:is="hasSubLinks ? 'button' : 'router-link'"
:to="hasSubLinks ? undefined : href"
:some-prop="computedValue"
#click="hasSubLinks ? handleClick() : navigate"
>
<!-- arbitrary markup -->
</component>
...
computed: {
computedValue() {
if (this.hasSubLinks) return 'something';
if (this.day === 'Friday') return 'tgif';
return 'its-fine';
},
},
But I basically always wrap router-link, so you can gain control over disabled state, or pre-examine any state or props before rendering the link, with something like this:
<template>
<router-link
v-slot="{ href, route, navigate, isActive, isExactActive }"
:to="to"
>
<a
:class="['nav-link-white', {
'nav-link-white-active': isActive,
'opacity-50': isDisabled,
}]"
:href="isDisabled ? undefined : href"
#click="handler => handleClick(handler, navigate)"
>
<slot></slot>
</a>
</router-link>
</template>
<script>
export default {
name: 'top-nav-link',
props: {
isDisabled: {
type: Boolean,
required: false,
default: () => false,
},
to: {
type: Object,
required: true,
},
},
data() {
return {};
},
computed: {},
methods: {
handleClick(handler, navigate) {
if (this.isDisabled) return undefined;
return navigate(handler);
},
},
};
</script>
In my app right now, I'm noticing that some combinations of #click="handler => handleClick(handler, navigate)" suffer significantly in performance.
For example this changes routes very slow:
#click="isDisabled ? undefined : handler => navigate(handler)"
But the pattern in my full example code above works and has no performance issue.
In general, ternary operator in #click can be very dicey, so if you get issues, don't give up right away, try many different ways to bifurcate on predicates or switch over <component :is="" based on state. navigate itself is an ornery one because it requires the implicit first parameter to work.
I haven't tried, but you should be able to use something like Function.prototype.call(), Function.prototype.apply(), or Function.prototype.bind().
For example, you might be able to do:
#click="handler => setupNavigationTarget(handler, navigate)"
...
setupNavigationTarget(handler, cb) {
if (this.isDisabled) return undefined;
return this.$root.$emit('some-event', cb.bind(this, handler));
},
...
// another component
mounted() {
this.$root.$on('some-event', (navigate) => {
if (['Saturday', 'Sunday'].includes(currentDayOfTheWeek)) {
// halt the navigation event
return undefined;
}
// otherwise continue (and notice how downstream logic
// no longer has to care about the bound handler object)
return navigate();
});
},
You could also use the following:
<router-link class="Card__link" :to="id ? { name: 'Property', params: { id: id }} : {}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If id is undefined the router won't redirect the page to the link.
I've tried different solutions but only one worked for me, maybe because I'm running if from Nuxt? Although theoretically nuxt-link should work exactly the same as router-link.
Anyway, here is the solution:
<template>
<router-link
v-slot="{ navigate }"
custom
:to="to"
>
<button
role="link"
#click="onNavigation(navigate, $event)"
>
<slot></slot>
</button>
</router-link>
</template>
<script>
export default {
name: 'componentName',
props: {
to: {
type: String,
required: true,
},
},
methods: {
onNavigation(navigate, event) {
if (this.to === '#other-action') {
// do something
} else {
navigate(event);
}
return false;
},
};
</script>