How to use conditional rendering with Vue.js slots? - javascript

How can I use the result of a v-if to render two different components in Vue? If it matters, I'm also trying to do this while rendering a TreeView using v-for. Here's what I tried so far.
TreeView.vue:
<template>
<span>
<ul v-show="isOpen" v-if="isFolder">
<p
v-for="(child, index) in item.children"
v-bind:item="child"
v-bind:key="index"
<span v-if="!child.isFolder">
<slot v-bind:individualBookmark="child"></slot>
</span>
<span v-else>
<div v-bind:class="{bold: isFolder}" v-on:click="toggle" v-on:dblclick="makeFolder" v-on:contextmenu="rightClick">
{{ child.name }}
<span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span>
</div>
</span>
</p>
</ul>
</span>
</template>
<script>
export default {
name: "TreeView",
props: {
item: Object
},
components: {
},
data: function() {
return {
isOpen: false
}
},
computed: {
isFolder: function() {
return this.item.children.length > 0;
//return this.item.children && this.item.children.length;
}
},
methods: {
toggle: function() {
if (this.isFolder) {
this.isOpen = !this.isOpen;
}
},
makeFolder: function() {
if (!this.isFolder) {
this.$emit("make-folder", this.item);
this.isOpen = true;
}
},
rightClick: function() {
alert("Right click action")
}
}
}
</script>
App.vue (the Bookmark tag is a custom component I built that does not have the isFolder computed property):
<template>
<div id="app">
<TreeView v-slot:default="slotProps"
v-bind:item="treeData"
v-on:make-folder="makeFolder"
v-on:add-item="addItem">
<Bookmark v-bind="slotProps.individualBookmark"></Bookmark>
</TreeView>
</div>
</template>
<script>
import Bookmark from './components/Bookmark.vue'
import TreeView from './components/TreeView.vue'
import Vue from 'vue'
export default {
name: 'App',
components: {
Bookmark,
TreeView
},
data: function() {
return {
treeData : {
name: "My Tree",
children: [
{ name: "hello" },
{ name: "wat" },
{
name: "child folder 1",
children: [
{
name: "child folder",
children: [{ name: "hello" }, { name: "wat" }]
},
{ name: "hello" },
{ name: "wat" },
{
name: "child folder",
children: [{ name: "hello" }, { name: "wat" }]
}
]
}
]
}
}
},
methods: {
makeFolder: function(item) {
Vue.set(item, "children", []);
this.addItem(item);
},
addItem: function(item) {
item.children.push({
name: "new stuff"
});
}
}
}
</script>
When I run this, for some reason, the v-if="!child.isFolder" in TreeView.vue always evaluates to true so it renders the Bookmark component. Even in the case when the v-if should be evaluating to false like when given the "child folder 1" child object of treeData, it still evaluates to true. I have a feeling the problem is to do with how I'm using <slot></slot> in TreeView.vue but I'm not sure. Why is the v-if always evaluating to true and never false?
The problem may also have to do with how I wrote the isFolder property.

Add a computed property called computedChildren inside the Treeview component which maps the item prop by adding isFolder property :
computed: {
isFolder: function() {
return this.item.children.length > 0;
//return this.item.children && this.item.children.length;
},
computedChildren(){
return this.item.children.map(child=>{
child.isFolder=child.children?child.children.length>0:false //this add the property isFolder
return child
})
}
},
Then loop through it as follows :
<p
v-for="(child, index) in computedChildren"
...
For replacing the slot you could use template element :
<TreeView
v-bind:item="treeData"
v-on:make-folder="makeFolder"
v-on:add-item="addItem">
<template v-slot:default="slotProps">
<Bookmark v-bind="slotProps.individualBookmark"></Bookmark>
</template>
</TreeView>

Related

How to render an specific task by id from an store in Vuex

I am trying to render a specific id when i click on that taskId. Right now when i click on it, it takes me to the route /tasks/details/id# and shows me the specific id in the URL but the details page shows me all the tasks instead of the specific one i want to see. I would really appreciate the time to review this and Thank you in advance.
in VUEX:
import { createStore } from "vuex";
export default createStore({
state: {
tasks: [
{
id: 1,
projectType: "Web Development",
taskName: "Fixing Bug",
projectDescription: "Task Assigned to fix navbar bug",
dateCreated: "12/06/2021",
duration: 1,
status: "Completed",
completeDate: "12/06/2021",
notes: "difficult task, i posponed copuple of times",
},
{
id: 2,
projectType: "Python",
taskName: "Tutorials",
projectDescription: "Looking at python tutorials",
dateCreated: "12/06/2021",
duration: 1,
status: "Completed",
completeDate: "",
notes: "difficult task, i posponed copuple of times",
taskHistory: [],
},
],
},
mutations: {
addTask(state, payload) {
state.tasks.unshift(payload);
},
},
actions: {
addTask(context, data) {
const registerTask = {
id: Math.floor(Math.random() * 1000),
projectType: data.project,
taskName: data.task,
status: data.status,
duration: data.duration,
created: data.created,
completeDate: data.completed,
notes: data.notes,
history: data.history,
projectDescription: data.desc,
};
context.commit("addTask", registerTask);
},
},
getters: {
taskList(state) {
return state.tasks;
},
getByLength(state) {
return state.tasks.length;
},
getId: (state) => (id) => {
return state.tasks.find((task) => task.id === id);
},
},
modules: {},
});
In TASKLIST page:
<template>
<base-card class="wrapper">
<h2>Task Details</h2>
<base-button link :to="'/tasks/add'">Add task</base-button>
</base-card>
<items-list
v-for="task in tasks"
:key="task.id"
:id="task.id"
:project="task.projectType"
:description="task.projectDescription"
:task="task.taskName"
:duration="task.duration"
:created="task.dateCreated"
:complete="task.dateCompleted"
:status="task.status"
>
</items-list>
</template>
<script>
import ItemsList from "../components/layout/ItemsList.vue";
export default {
data() {
return {
};
},
components: { ItemsList },
computed: {
tasks() {
return this.$store.getters.taskList;
},
detailsLink() {
// id# passed from ItemsList to find specific id
return "/tasks/details/" + this.id;
},
},
};
ITEMSLIST Component: (component that pass the props to TaskList)
<template>
<section>
<base-card class="wrapper">
<ul>
<li>
<h3>{{ project }}</h3>
</li>
<li><span>Task Name:</span> {{ task }}</li>
<li><span> Date Created:</span>{{ created }}</li>
<li><span> Status: </span> {{ status }}</li>
<li><span>Duration: </span>{{ duration }} hr</li>
<li><span>Description: </span> {{ description }}</li>
<li><span>Id#</span> {{ id }}</li>
<div class="buttons">
<base-button link :to="'/tasks/details/' + this.id"
>Details</base-button
>
</div>
</ul>
</base-card>
</section>
</template>
<script>
import BaseCard from "../ui/BaseCard.vue";
import BaseButton from "../BaseButton.vue";
export default {
props: [
"id",
"project",
"task",
"created",
"status",
"duration",
"description",
],
components: {
BaseCard,
BaseButton,
},
data() {
return {
};
},
computed: {
tasks() {
return this.$store.getters.taskList;
},
getByLength() {
return this.$store.getters.getByLength;
},
// getId() {
// return this.$store.getters.taskList.find((task) => task.id === this.id);
// },
},
};
</script>
And finally, the TaskDetails Page(Will show details using id passed from a prop from the state.$store from VUEX:
<template>
<base-card class="wrapper">
<h2>Task Details</h2>
</base-card>
<task-items
v-for="task in tasks"
:key="task.id"
:id="task.id"
:project="task.projectType"
:description="task.projectDescription"
:name="task.taskName"
:duration="task.duration"
:created="task.dateCreated"
:complete="task.dateCompleted"
:status="task.status"
:notes="task.notes"
:history="task.history"
></task-items>
<router-view></router-view>
</template>
<script>
import BaseCard from "../components/ui/BaseCard.vue";
import TaskItems from "../components/layout/TaskItems.vue";
export default {
components: { BaseCard, TaskItems },
data() {
return {};
},
computed: {
tasks() {
return this.$store.getters.taskList;
},
getById() {
return this.$store.getters.getId;
},
taskId() {
return this.$store.getters.getId.id;
},
},
};
</script>

Vue - define event handlers in array of dynamic components

I want do create some some different components by looping through an array of components like in my example. But I want to create different event handlers for each component. How can I define them in my componentData Array and bind them while looping?
componentData: [
{ name: TestPane, props: { data: "hello" }, id: 1 },
{ name: TestPane, props: { data: "bye" }, id: 2 },
],
]
<div v-for="component in componentData" :key="component.id">
<component v-bind:is="component.name" v-bind="component.props">
</component>
</div>
You can use the v-on directive. Let's understand how Vue bind your event listeners to the component first:
When you add a #input to a componnet what you are actualy doing is v-on:input. Did you notice the v-on over there? This means that you are actually passing an 'object of listeners' to the component.
Why not pass all of them in one go?
<template>
<section>
<div v-for="component in componentData" :key="component.id">
<component v-bind:is="component.name" v-bind="component.props" v-on="component.on">
</component>
</div>
</section>
</template>
<script>
export default {
data: () => ({
componentData: [
{ name: TestPane, props: { data: "hello" }, id: 1, on: { input: (e) => { console.log(e) } } },
{ name: TestPane, props: { data: "bye" }, id: 2, on: { input: (e) => { console.log(e); } } },
],
})
}
</script>
As you could guess you can listen to the events now inside of on object. You can add more if you would like as well:
{
name: TestPane,
props: { data: "hello" },
id: 1,
on: {
input: (e) => { console.log(e) },
hover: (e) => { console.log('This component was hovered') }
}
}
Add method names to your array like :
componentData: [
{ name: TestPane, props: { data: "hello" }, id: 1, method:"method1" },
{ name: TestPane, props: { data: "bye" }, id: 2 ,method:"method2"},
],
in template :
<component ... #click.native="this[component.method]()">
or add another method called handler which runs the appropriate component method :
<component ... #click.native="handler(component.method)">
methods:{
handler(methodName){
this[methodName]();
}
...
}
if the events are emitted from components, you should add their names and bind them dynamically :
componentData: [
{ name: TestPane, props: { data: "hello" }, id: 1,event:'refresh', method:"method1" },
{ name: TestPane, props: { data: "bye" }, id: 2 ,event:'input',method:"method2"},
],
<component ... #[component.event]="handler(component.method)">

Data not being passed to Vue component

I have a Vue component receiving an array of 'items' from its parent.
I've sorted them into categories, two 'items' in each category:
computed: {
// sort items into categories
glass: function() {
return this.items.filter(i => i.category === "glass").slice(0, 2);
},
ceramics:
// etc...
I need to place both items in categories.items to then pass them as props to another component:
data() {
return {
categories: [
{ name: "Glass", sort: "glass", items: {} },
{ name: "Ceramics", sort: "ceramics", items: {} },
{ name: "Brass", sort: "brass", items: {} },
{ name: "Books/Comics", sort: "books", items: {} },
{ name: "Collectibles", sort: "collectibles", items: {} },
{ name: "Pictures", sort: "pictures", items: {} },
{ name: "Other", sort: "other", items: {} }
]
};
},
When I use created or mounted nothing is passed through, when I use beforeDestroy or destroy and console.log the results it works fine, but, they're of no use when exiting the page.
The 'items' are from an Axios GET request, could this be why?
GET request from parent component:
methods: {
fetchItems() {
// items request
let uri = "http://localhost:8000/api/items";
this.axios.get(uri).then(response => {
// randomize response
for (let i = response.data.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[response.data[i], response.data[j]] = [
response.data[j],
response.data[i]
];
}
this.items = response.data;
});
}
},
Passing props to child component:
<div
class="items-container"
v-for="category in categories"
:key="category.name"
>
<router-link :to="'category.link'" class="category-names-home-link">
<h2 class="category-names-home">{{ category.name }}</h2>
</router-link>
<router-link
:to="'category.link'"
class="home-view-all"
#mouseover.native="expand"
#mouseout.native="revert"
>View All...</router-link
>
<div class="items">
<!-- Pass to child component as props: -->
<SubItem :item="categories.items" />
<SubItem :item="categories.items" />
</div>
</div>
Don't bother adding the items to the categories, keep them separate
Instead of multiple computeds, use one computed object hash to store all the filtered sets:
computed: {
filtered() {
if (!this.items) return null;
const filtered = {};
this.items.forEach(item => {
if (filtered[item.category]) {
filtered[item.category].push(item);
} else {
filtered[item.category] = [item];
}
});
return filtered;
}
}
Result:
{
'Glass': [ ... ],
'Ceramic': [ ... ]
...
}
In the template:
<div>
<div v-for="category in categories" :key="category.name">
<div class="items" v-for="item in filtered[category.name]">
<SubItem :item="item" />
</div>
</div>
</div>
You can use a v-if in the parent to prevent displaying anything until the data is loaded:
<display v-if="items" :items="items"></display>
Here is a demo

vue v-for object props

I am trying to pass an array object to a component, but am not getting anything
create.vue
<el-row >
<component1 v-for="product in products" :value="product" :key="product.id"></component1>
</el-row>
//script section
data() {
return {
products: [] //there have (id = 1, name = first), (id = 2, name = second)
}
}
component1.vue
<el-row>
<div>
{{ product.name }}
</div>
</el-row>
export default {
props: ['value'],
watch: {
value: {
hander: function (val) {
console.log(val);
this.product = {
id: val.id,
name: val.name
}
},
deep: true
}
},
data() {
return {
product: {
id: null,
name: null
}
}
},
but watch not worked ( {{product.name}} its null), why? or how fixed this?
You're trying to watch a property that doesn't change, in your example i don't figure out the need of using watch property, but you could achieve that by assigning value to your data property called product in the mounted life cycle hook as follows :
<el-row>
<div>
{{ product.name }}
</div>
</el-row>
export default {
props: ['value'],
watch: {
value: {
handler: function (val) {
console.log(val);
this.product = {
id: val.id,
name: val.name
}
},
deep: true
}
},
data() {
return {
product: {
id: null,
name: null
}
}
},
mounted(){
this.product= {
id: this.value.id,
name: this.value.name
}
}

Multiple select Vue.js and computed property

I'm using Vue.js 2.0 and the Element UI library.
I want to use a multiple select to attribute some roles to my users.
The list of all roles available is received and assigned to availableRoles. Since it is an array of object and the v-model accepts only an array with value, I need to extract the id of the roles trough the computed property computedRoles.
The current roles of my user are received and assigned to userRoles: [{'id':1, 'name':'Admin'}, {'id':3, 'name':'User'}].
computedRoles is then equals to [1,3]
The preselection of the select is fine but I can't change anything (add or remove option from the select)
What is wrong and how to fix it?
http://jsfiddle.net/3ra1jscx/3/
<div id="app">
<template>
<el-select v-model="computedRoles" multiple placeholder="Select">
<el-option v-for="item in availableRoles" :label="item.name" :value="item.id">
</el-option>
</el-select>
</template>
</div>
var Main = {
data() {
return {
availableRoles: [{
id: 1,
name: 'Admin'
}, {
id: 2,
name: 'Power User'
}, {
id: 3,
name: 'User'
}],
userRoles: [{'id':1, 'name':'Admin'}, {'id':3, 'name':'User'}]
}
},
computed : {
computedRoles () {
return this.userRoles.map(role => role.id)
}
}
}
I agree mostly with #wostex answer, but he doesn't give you the userRoles property back. Essentially you should swap computedRoles and userRoles. userRoles becomes a computed property and computedRoles is a data property. In my update, I changed the name of computedRoles to selectedRoles.
var Main = {
data() {
return {
availableRoles: [{
id: 1,
name: 'Admin'
}, {
id: 2,
name: 'Power User'
}, {
id: 3,
name: 'User'
}],
selectedRoles:[1,2]
}
},
computed : {
userRoles(){
return this.availableRoles.reduce((selected, role) => {
if (this.selectedRoles.includes(role.id))
selected.push(role);
return selected;
}, [])
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
And here is the fiddle.
Check the solution: jsfiddle
The caveat here is that computed properties are getters mainly. You can define setter for computed property, but my approach is more vue-like in my opinion.
In short, instead of v-model on computed set v-model for data property.
Full code:
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui/lib/index.js"></script>
<div id="app">
<template>
<el-select v-model="ids" multiple placeholder="Select" #change="logit()">
<el-option v-for="item in availableRoles" :label="item.name" :value="item.id">
</el-option>
</el-select>
</template>
</div>
var Main = {
data() {
return {
availableRoles: [{
id: 1,
name: 'Admin'
}, {
id: 2,
name: 'Power User'
}, {
id: 3,
name: 'User'
}],
userRoles: [{'id':1, 'name':'Admin'}, {'id':3, 'name':'User'}],
ids: []
}
},
mounted() {
this.ids = this.userRoles.map(role => role.id);
},
methods: {
logit: function() {
console.log(this.ids);
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')

Categories

Resources