Dynamic Components with Slots - vue.js

How can I use named slots from dynamic components in the parent component?
A slider component takes an array of dynamic slide components:
<slider :slides="slides" />
Each slide has named slots with content to be using by the slider:
<template>
<div class="slide">
<div slot="main">Slide 1 Main</div>
<div slot="meta">Slide 1 Meta</div>
</div>
</template>
The slider should now use these slots, like so:
<template>
<div class="slider">
<div class="slider__slide" v-for="slide in slides">
<component :is="slide">
<div class="slider__slide__main">
<slot name="main" /><!-- show content from child's slot "main" -->
</div>
<div class="slider__slide__meta">
<slot name="meta" /><!-- show content from child's slot "meta" -->
</div>
</component>
</div>
</div>
</template>
But <component> ignores its inner content, so the slots are ignored.
Example:
https://codepen.io/anon/pen/WZjENK?editors=1010
If this isn't possible, is there another way to create a slider that takes HTML content from slide components without caring about their content?

By splitting the main/meta sections into their own components, you can relatively easily use a render function to split them into the sections you want.
console.clear()
const slide1Meta = {
template:`<div>Slide 1 Meta</div>`
}
const slide1Main = {
template: `<div>Slide 1 Main</div>`
}
const slide2Meta = {
template:`<div>Slide 2 Meta</div>`
}
const slide2Main = {
template: `<div>Slide 2 Main</div>`
}
Vue.component('slider', {
props: {
slides: {
type: Array,
required: true
}
},
render(h){
let children = this.slides.map(slide => {
let main = h('div', {class: "slider__slide__main"}, [h(slide.main)])
let meta = h('div', {class: "slider_slide_meta"}, [h(slide.meta)])
return h('div', {class: "slider__slide"}, [main, meta])
})
return h('div', {class: "slider"}, children)
}
});
new Vue({
el: '#app',
data: {
slides: [
{meta: slide1Meta, main: slide1Main},
{meta: slide1Meta, main: slide2Main}
]
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<slider :slides="slides" />
</div>
<script type="text/x-template" id="slide1-template">
<div class="slide">
<div slot="main">Slide 1 Main</div>
<div slot="meta">Slide 1 Meta</div>
</div>
</script>
<script type="text/x-template" id="slide2-template">
<div class="slide">
<div slot="main">Slide 2 Main</div>
<div slot="meta">Slide 2 Meta</div>
</div>
</script>

Actually slots within a dynamic component element do work. I have have been attempting to solve this same issue, and found this lovely little example by Patrick O'Dacre on CodePen. Patrick has made ample and useful comments in his code which I paste verbatim here for posterity. I omitted the css which you can find on CodePen.
const NoData = {
template: `<div>
This component ignores the data completely.
<p>But there are slots!</p>
<slot></slot>
<slot name="namedSlot"></slot>
</div>`
// In this component, I just ignore the props completely
}
const DefaultMessage = {
template: `<div>
This component will show the default msg: <div>{{parentData.msg}}</div>
</div>`,
// this component won't have posts like the Async Component, so we just ignore it
props: ['parentData']
}
const CustomMessage = {
template: `<div>
This component shows a custom msg: <div>{{parentData.msg}}</div>
</div>`,
// this component won't have posts like the Async Component, so we just ignore it
props: ['parentData']
}
const Async = {
template: `<div>
<h2>Posts</h2>
<p>{{parentData.msg}}</p>
<section v-if="parentData.posts.length > 0">
<ul>
<li class="postInfo" v-for="post in parentData.posts">
<div class="postInfo__title">
<strong>Title:</strong> {{post.title}}
</div>
</li>
</ul>
</section>
</div>`,
props: ['parentData']
}
/* Children should only affect parent properties via an EVENT (this.$emit) */
const ChangeMessage = {
template: `<div>
<p>Type here to change the message from the child component via an event.</p>
<div><input type="text" v-model="message" #input="updateDateParentMessage" /></div>
</div>`,
data() {
return {
// initialize our message with the prop from the parent.
message: this.parentData.msg ? this.parentData.msg : ''
}
},
props: ['parentData'],
/* Need to watch parentData.msg if we want to continue
to update this.message when the parent updates the msg */
watch: {
'parentData.msg': function (msg) {
this.message = msg
}
},
methods: {
updateDateParentMessage() {
this.$emit('messageChanged', this.message)
}
}
};
const Home = {
template: `<section>
<div class="wrap">
<div class="right">
<p><strong>Change the current component's message from the Home (parent) component:</strong></p>
<div><input type="text" v-model="dataForChild.msg" /></div>
<p><strong>Important!</strong> We do not change these props from the child components. You must use events for this.</p>
</div>
</div>
<div class="controls">
<button #click="activateComponent('NoData')">No Data</button>
<button #click="activateComponent('DefaultMessage')">DefaultMessage</button>
<button #click="activateComponent('CustomMessage', {posts: [], msg: 'This is component two'})">CustomMessage</button>
<button #click="getPosts">Async First</button>
<button #click="activateComponent('ChangeMessage', {msg: 'This message will be changed'})">Change Msg from Child</button>
<button #click="deactivateComponent">Clear</button>
</div>
<div class="wrap">
<div class="right">
<h2>Current Component - {{currentComponent ? currentComponent : 'None'}}</h2>
<!-- ATTN: Uncomment the keep-alive component to see what happens
when you change the message in ChangeMessage component and toggle
back and forth from another component. -->
<!-- <keep-alive> -->
<component
:is="currentComponent"
:parentData="dataForChild"
v-on:messageChanged="updateMessage">
<div class="slotData">This is a default slot</div>
<div slot="namedSlot" class="namedSlot">This is a NAMED slot</div>
<div slot="namedSlot" class="namedSlot"><p>Here we pass in the message via a slot rather than as a prop:</p>{{dataForChild.msg}}</div>
</component>
<!-- </keep-alive> -->
</div>
</div>
</section>`,
data() {
return {
currentComponent: false,
/* You don't NEED to put msg and posts here, but
I prefer it. It helps me keep track of what info
my dynamic components need. */
dataForChild: {
// All components:
msg: '',
// Async Component only
posts: []
}
}
},
methods: {
/**
* Set the current component and the data it requires
*
* #param {string} component The name of the component
* #param {object} data The data object that will be passed to the child component
*/
activateComponent(component, data = { posts: [], msg: 'This is a default msg.'}) {
this.dataForChild = data;
this.currentComponent = component;
},
deactivateComponent() {
this.dataForChild.msg = '';
this.currentComponent = false;
},
/* Hold off on loading the component until some async data is retrieved */
getPosts() {
axios.get('https://codepen.io/patrickodacre/pen/WOEXOX.js').then( resp => {
const posts = resp.data.slice(0, 10) // get first 10 posts only.
// activate the component ONLY when we have our results
this.activateComponent('Async', {posts, msg: `Here are your posts.`})
})
},
/**
* Update the message from the child
*
* #listens event:messageChanged
* #param {string} newMessage The new message from the child component
*/
updateMessage(newMessage) {
this.dataForChild.msg = newMessage
}
},
// must wire up your child components here
components: {
NoData,
CustomMessage,
DefaultMessage,
Async,
ChangeMessage
}
}
const routes = [
{ path: '/', name: 'home', component: Home}
];
const router = new VueRouter({
routes
});
const app = new Vue({
router
}).$mount("#app")
The html,
<div id="app">
<h1>Vue.js Dynamic Components with Props, Events, Slots and Keep Alive</h1>
<p>Each button loads a different component, dynamically.</p>
<p>In the Home component, you may uncomment the 'keep-alive' component to see how things change with the 'ChangeMessage' component.</p>
<nav class="mainNav"></nav>
<!-- route outlet -->
<!-- component matched by the route will render here -->
<section class="mainBody">
<router-view></router-view>
</section>
</div>

Related

Vue: Toggle Button with Data from Child to Parent

I have a parent component that has numerous containers. Each container has an image and some buttons.
I have over simplified the parent and child components below. When a button is clicked that is within the child component, I would like to toggle the class on an element that is in the parent container. I would like to effect each image individually, not globally. How do I do this?
parent:
<template>
<div>
<div :class="{ active: mock }">
<img src="/path">
</div>
<toggleButtons/>
</div>
<div>
<div :class="{ active: mock }">
<img src="/path">
</div>
<toggleButtons/>
</div>
</template>
<script>
import toggleButtons from './toggleButtons'
export default {
name: "parent",
components: {
toggleButtons
}
};
</script>
child:
<template>
<div class="switch-type">
<a #click="mock = false">Proto</a>
<a #click="mock = true">Mock</a>
</div>
</template>
<script>
export default {
name: "toggleButtons",
data() {
return {
mock: false
}
}
};
</script>
Oversimplified example of how you can pass data from child to parent:
Child:
<a #click="$emit('mockUpdated', false)">Proto</a>
<a #click="$emit('mockUpdated', true)">Mock</a>
Parent (template):
<toggleButtons #mockUpdated="doSomething" />
Parent(methods):
doSomething(value) {
// value will be equal to the second argument you provided to $emit in child
}
EDIT: (toggling the class for each individual container):
I would probably create a new component for the container (container.vue), pass a path as a prop :
<template>
<div>
<div :class="{ active: mock }">
<img :src="path">
</div>
<toggleButtons #mockUpdated="doSomething" />
</div>
</template>
<script>
export default {
props: {
path: String,
},
data() {
return {
mock: false
}
},
methods: {
doSomething(value) {
this.mock = value;
}
}
}
</script>
and then in Parent.vue, you can import the container component and use it like:
<template>
<Container path="/path-to-file.jpg" />
<Container path="/path-to-file.jpg" />
</template>
There is nothing to do with slots in your case. You need "emit event" to parent from button, and pass mock data to update img states. Slot is a very different thing. There are multiple ways to achive your goal. One way can be something like this:
parent
<AppImage v-for="img in imageData" :key="img.id" :image="img"/>
data() {
iamges: ["yourPath1", "yourPath2"]
},
computed: {
imageData() {
// adding ID is almost always a good idea while creating Vue apps.
return this.images.map(x => {
id: Math.floor(Math.random() * 1000),
path: x
})
}
}
Image.vue
<img :path="image.path" :class={ active: mock} />
<toggleButtons #toggled=changeImageState />
props: [image],
data() {
mock: ''
},
methods: {
changeImageState(event) {
this.mock = event
}
}
ToggleButtons.vue
<a #click="toggleMock(false)">Proto</a>
<a #click="toggleMock(true)">Mock</a>
emits: ['toggled']
methods: {
toggleMock(val) {
this.$emits('toggled', val)
}
}
Please read the code and let me know if you have any question.

How can i add a confirmation Pop up modal with Vue Draggable?

I have an vue component which uses Vue Draggable .
<template>
<div class="row my-5">
<div v-for="column in columns" :key="column.title" class="col">
<p class="font-weight-bold text-uppercase">{{column.title}}</p>
<!-- Draggable component comes from vuedraggable. It provides drag & drop functionality -->
<draggable :list="column.tasks" :animation="200" ghost-class="ghost-card" group="tasks" :move="checkMove">
<transition-group>
<task-card
v-for="(task) in column.tasks"
:key="task.id"
:task="task"
class="mt-3 cursor-move"
></task-card>
<!-- </transition-group> -->
</transition-group>
</draggable>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import TaskCard from "../board/TaskCard";
export default {
name: "App",
components: {
TaskCard,
draggable,
},
data() {
return {
columns: [
.....
],
};
},
methods: {
checkMove: function(evt){
console.log('moved');
}
},
};
</script>
In TaskCard Component -
<template>
<div class="bg-white shadow rounded p-3 border border-white">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>{{task.id}}</h2>
<span>{{task.date}}</span>
</div>
<p class="font-weight-bold">{{task.title}}</p>
</div>
</template>
<script>
export default {
props: {
task: {
type: Object,
default: () => ({}),
},
},
};
</script>
When I move an item, I want a modal that confirms the change and only then move the item.
(ie. if I click on the cancel button inside the modal, the item should not be moved.)
How can this be achieved using the checkMove() function provided?
I don't think you can achieve this by using onMove event. The onEnd event it seems more suitable but unfortunately it doesn't have any cancel drop functionality.
So I think the only solution here is revert it back if the user decides to cancel.
You can listen on change event (See more in documentation)
<draggable
group="tasks"
v-model="column.tasks"
#change="handleChange($event, column.tasks)">
...
</draggable>
...
<button #click="revertChanges">Cancel</button>
<button #click="clearChanges">Yes</button>
And
...
handleChange(event, list) {
this.changes.push({ event, list })
this.modal = true
},
clearChanges() {
this.changes = []
this.modal = false
},
revertChanges() {
this.changes.forEach(({ event, list }) => {
if (event.added) {
let { newIndex } = event.added
list.splice(newIndex, 1)
}
if (event.removed) {
let { oldIndex, element } = event.removed
list.splice(oldIndex, 0, element)
}
if (event.moved) {
let { newIndex, oldIndex, element } = event.moved
list[newIndex] = list[oldIndex]
list[oldIndex] = element
}
})
this.changes = []
this.modal = false
}
...
JSFiddle example

Only show slot if it has content, when slot has no name?

As answered here, we can check if a slot has content or not. But I am using a slot which has no name:
<template>
<div id="map" v-if="!isValueNull">
<div id="map-key">{{ name }}</div>
<div id="map-value">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
name: {type: String, default: null}
},
computed: {
isValueNull() {
console.log(this.$slots)
return false;
}
}
}
</script>
I am using like this:
<my-map name="someName">{{someValue}}</my-map>
How can I not show the component when it has no value?
All slots have a name. If you don't give it a name explicitly then it'll be called default.
So you can check for $slots.default.
A word of caution though. $slots is not reactive, so when it changes it won't invalidate any computed properties that use it. However, it will trigger a re-rendering of the component, so if you use it directly in the template or via a method it should work fine.
Here's an example to illustrate that the caching of computed properties is not invalidated when the slot's contents change.
const child = {
template: `
<div>
<div>computedHasSlotContent: {{ computedHasSlotContent }}</div>
<div>methodHasSlotContent: {{ methodHasSlotContent() }}</div>
<slot></slot>
</div>
`,
computed: {
computedHasSlotContent () {
return !!this.$slots.default
}
},
methods: {
methodHasSlotContent () {
return !!this.$slots.default
}
}
}
new Vue({
components: {
child
},
el: '#app',
data () {
return {
show: true
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button #click="show = !show">Toggle</button>
<child>
<p v-if="show">Child text</p>
</child>
</div>
Why you dont pass that value as prop to map component.
<my-map :someValue="someValue" name="someName">{{someValue}}</my-map>
and in my-map add prop:
props: {
someValue:{default: null},
},
So now you just check if someValue is null:
<div id="map" v-if="!someValue">
...
</div

VuesJS components template

I'm a VueJS beginner and i'm struggling to understand some component logic.
If i have my component (simplified for clarity) :
Vue.component('nav-bar', {
template: '<nav [some code] ></nav>'
}
This component represent the whole navigation bar of my page.
In my HTML file, how can i insert code inside the component?
Something like:
<nav-bar>
<button></button>
...
</nav-bar>
Could you please tell me if it is the right way to do it?
There are at least three options I can think of:
Using ref, or
Slot props with scoped slots, or
provide/inject.
1. Example with ref
Vue.component('NavBar', {
template: `
<nav>
<slot></slot>
</nav>
`,
methods: {
run() {
console.log('Parent\'s method invoked.');
}
}
});
new Vue().$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<nav-bar ref="navbar">
<button #click="$refs.navbar.run()">Run with refs</button>
</nav-bar>
</div>
2. With Scoped <slot>
Vue.component('NavBar', {
template: `
<nav>
<slot v-bind="$options.methods"></slot>
</nav>
`,
methods: {
run() {
console.log('Parent\'s method invoked.');
}
}
});
new Vue().$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<nav-bar>
<template #default="methods">
<button #click="methods.run">Run with slot props</button>
</template>
</nav-bar>
</div>
3. With provide and inject
Vue.component('NavBar', {
template: `
<nav>
<slot></slot>
</nav>
`,
provide() {
const props = {
...this.$options.methods,
// The rest of props you'd like passed down to the child components.
};
return props;
},
methods: {
run() {
console.log('Parent\'s method invoked.');
}
}
});
// In order to "receive" or `inject` the parent props,
// the child(ren) needs to be a component itself.
Vue.component('Child', {
template: `
<button #click="run">
<slot></slot>
</button>
`,
// Inject anything `provided` by the direct parent
// This could also be `data` or `props`, etc.
inject: ['run']
});
new Vue().$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<nav-bar>
<template>
<child>Run with injected method</child>
</template>
</nav-bar>
</div>

Vue.js: How to access property value of objects in loop in a child component

I'm working on a Vue app in which my initial view (projects.vue) renders an array of objects (using v-for loop), in summary form; each object's summary has button links to a 'detail' view & and 'edit' view; the links use an object property (id) to select/render the corresponding detail view, and this is working fine. Now, since these links will occur in several other views, I made a buttons.vue component and import it in projects.vue, but this way they're not getting the ID, and as a newb despite some reading about components and props I haven't figured how to achieve this seemingly simple function. My components:
projects.vue (this works; note the use of 'project.id'):
<template>
<ol class="projects">
<li class="project" v-for="(project, ix) in projects">
<!— SNIP: some html showing the project summary —>
<!--
my objective is to replace the following with
<app-buttons></app-buttons>
-->
<div class="buttons">
<router-link class="button" v-bind:to="'/project-detail/' + project.id">details</router-link>
<router-link class="button" v-bind:to="'/project-edit/' + project.id">edit</router-link>
</div><!-- END .buttons -->
</li>
</ol>
</template>
<script>
export default {
components: {
'app-buttons': Buttons
},
data () {
return {
projects: [],
search: "",
projectID: ""
}
}, // data
methods: {
}, // methods
created: function() {
this.$http.get('https://xyz.firebaseio.com/projects.json')
.then(function(data){
return data.json();
}).then(function(data){
var projectsArray = [];
for (let key in data) {
data[key].id = key; // add key to each object
this.projectID = key;
projectsArray.push(data[key]);
}
this.projects = projectsArray;
})
}, // created
computed: {
}, // computed
} // export default
</script>
buttons.vue:
<template>
<div class="buttons">
<router-link class="button" v-bind:to="'/project-detail/' + projectID">details</router-link>
<router-link class="button" v-bind:to="'/project-edit/' + projectID">edit</router-link>
</div><!-- END .buttons -->
</template>
<script>
export default {
props: [ "projectID" ],
data() {
return {
}
},
methods: {}
}
</script>
<app-buttons :projectId="foo"></app-buttons> then inside your component,
<template>
<div class="buttons">
<p>{{ this.projectId}}</p>
<router-link class="button" v-bind:to="'/project-detail/' + projectID">details</router-link>
<router-link class="button" v-bind:to="'/project-edit/' + projectID">edit</router-link>
</div><!-- END .buttons -->
</template>
<script>
export default {
props: [ "projectId" ],
data() {
return {
}
},
methods: {}
}
</script>
Should show your project Id within the <p></p> tags.