Nested components not re-rendering properly: VueJs - javascript

I'm new to Vue and I'm building this forum kind of thing which can add nested comments in it. In here there are two components. PostForum and Comment. PostForum contains an input box and parent Comments. And inside each comment, I added child comments recursively.
When I adding comments, It works fine. But when deleting, it sends the ajax req but there's no re-rendering.
So this is how I designed it. When deleting a comment, I emit a global event and in PostForum component I listen to that event and deleting that comment from its data. So isn't that supposed to re-render all the comments accordingly? Can anyone tell me what am I doing wrong here?
PostForum.vue
<template>
<!-- comment box here -->
<comment
v-for="(comment, index) in comments"
v-if="!comment.parent_id"
:reply="true"
:initialChildren="getChildren(comment.id)"
:key="index"
:comment="comment">
</comment>
</template>
<script>
export default {
data () {
return {
comments: [], // all comments
comment: { // new comment [at comment box]
body: '',
parent_id: 0,
},
}
},
methods: {
deleteComment (node) {
axios.delete(`/comments/${node.id}`)
.then(res => {
this.comments.splice(node.key, 1)
})
.catch(err => {
console.log(err)
})
},
getChildren: function (parent_id) {
return this.comments.filter(child => parent_id == child.parent_id)
},
},
mounted: function () {
window.Event.$on('comment-deleted', (node) => this.deleteComment(node))
}
}
</script>
Comment.vue
<template>
<button #click="deleteComment">X</button>
<!-- comment body goes here -->
<comment v-for="(child, i) in children" :key="i" :reply="false" :comment="child"></comment>
<!-- reply form here -->
</template>
<script>
export default {
props: ['initialChildren']
data: function () {
return {
newComment: {
body: '',
parent_id: this.comment.id,
},
children: this.initialChildren,
}
},
methods: {
deleteComment () {
window.Event.$emit('comment-deleted', {key: this.$vnode.key, id: this.comment.id})
},
}
}
</script>

I've tried this:
This code is just an example that may help you. In my case, child component is comment component in your case, and each child component has its own #action listener for his child component. So, he can use that to modify his own childrens.
Here is an example on codesandbox: https://codesandbox.io/s/qzrp4p3qw9
ParentComponent
<template>
<div>
<Child v-for="(children,index) in childrens" :child="children" :key="index" :parent="0" :pos="index"></Child>
</div>
</template>
import Child from './child';
export default {
data() {
return {
childrens:[
{
name:"a",
childrens:[
{
name:'aa',
},
{
name:'ba',
childrens:[
{
name:'baa',
childrens:[
{
name:'baaa',
},
{
name:'baab',
}
]
}
]
}
]
},
{
name:"a",
childrens:[
{
name:'aa',
},
{
name:'ab',
childrens:[
{
name:'aba',
childrens:[
{
name:'abaa',
childrens:[
{
name:'baa',
childrens:[
{
name:'baaa',
},
{
name:'baa',
}
]
}
]
},
{
name:'abab',
}
]
}
]
}
]
}
]
}
},
components:{
Child
}
}
ChildComponent
<template>
<div>
<div style="padding:5px">
{{ child.name }}
<button #click="deleteComment(child)">x</button>
</div>
<child #delete="deleteSubComment" style="padding-left:15px" v-if="typeof child.childrens !== 'undefined'" v-for="(children,index) in child.childrens" :child="children" :pos="index" :key="index" :parent="children.parent"></child>
</div>
</template>
export default {
name:"child",
props:['child','parent',"pos"],
methods:{
deleteComment(child) {
this.$emit('delete',child);
},
deleteSubComment(obj) {
this.child.childrens.splice(this.child.childrens.indexOf(obj),1);
}
}
}

Related

Issue when trying to interact with an API in Vuejs?

datalist.js
import axios from "axios";
export const datalist = () => {
return axios.get("myapiurl/name...").then((response) => response);
};
HelloWorld.vue
<template>
<div>
<div v-for="item in items" :key="item.DttID">
<router-link
:to="{
name: 'UserWithID',
params: { id: item.DepaD },
query: { DepaD: item.DepaID },
}"
>
<div class="bt-color">{{ item.DepaName }}</div>
</router-link>
</div>
<br /><br /><br />
<User />
</div>
</template>
<script>
import User from "./User.vue";
import { datalist } from "./datalist";
export default {
name: "HelloWorld",
components: {
User,
},
data() {
return {
items: datalist,
};
},
mounted() {
datalist().then((r) => {
this.items = r.data;
});
},
};
</script>
User.vue
<template>
<div>
<div v-for="(item, key) in user" :key="key">
{{ item.Accv }}
</div>
</div>
</template>
<script>
import { datalist } from "./datalist";
export default {
name: "User",
data() {
return {
lists: datalist,
};
},
computed: {
user: function () {
return this.lists.filter((item) => {
if (item.DepaD === this.$route.params.id) {
return item;
}
});
},
},
};
</script>
Error with the code is,
[Vue warn]: Error in render: "TypeError: this.lists.filter is not a function"
TypeError: this.lists.filter is not a function
The above error i am getting in User.vue component in the line number '20'
From the api which is in, datalist.js file, i think i am not fetching data correctly. or in the list filter there is problem in User.vue?
Try to change the following
HelloWorld.vue
data() {
return {
items: [],
};
},
mounted() {
datalist().then((r) => {
this.items = r.data;
});
},
User.vue
data() {
return {
lists: []
};
},
mounted() {
datalist().then((r) => {
this.lists = r.data;
});
},
At least this suppress the error, but i cant tell more based on your snippet since there are network issues :)
Since your datalist function returns a Promise, you need to wait for it to complete. To do this, simply modify your component code as follows:
import { datalist } from "./datalist";
export default {
name: "User",
data() {
return {
// empty array on initialization
lists: [],
};
},
computed: {
user: function() {
return this.lists.filter((item) => {
if (item.DeploymentID === this.$route.params.id) {
return item;
}
});
},
},
// asynchronous function - because internally we are waiting for datalist() to complete
async-mounted() {
this.users = await datalist() // or datalist().then(res => this.users = res) - then async is not needed
}
};
now there will be no errors when initializing the component, since initially lists is an empty array but after executing the request it will turn into what you need.
You may define any functions and import them, but they wont affect until you call them, in this case we have datalist function imported in both HelloWorld and User component, but it did not been called in User component. so your code:
data() {
return {
lists: datalist,
};
},
cause lists to be equal to datalist that is a function, no an array! where .filter() should be used after an array, not a function! that is the reason of error.
thus you should call function datalist and put it's response in lists instead of putting datalist itself in lists
Extra:
it is better to call axios inside the component, in mounted, created or ...
it is not good idea to call an axios command twice, can call it in HelloWorl component and pass it to User component via props

Change content on page on clicking a vue js child element

I am creating an app that will display products on a page. Each of these products have a list of features such as "battery power", "charge time", and even just a description that will vary per feature. My question is, how can I make a clickable element, that when clicked will find the data associated with that button/icon, then update the content on the page to reflect this? This content may or may not be in some kind of v-for loop.
See the example of what I have and what I want to achieve below.
Child component:
<template>
<li>
<button #click="$emit('changeProductData', feature)">
<img :src="require('../assets/images/' + feature.item.img)" />
</button>
</li>
</template>
<script>
export default {
props: {
feature: Object
}
}
</script>
Parent component:
<template>
<div>
<div v-for="product in getProduct(productId)" :key="product.productId">
{{ product }}
<Halo
:featuresCount="
`circle-container-` + product.features.length.toString()
"
>
<Feature
v-for="(feature, key, index) in product.features"
:key="index"
:feature="feature"
#changeProductData="something" // this is where we call the custom event
></Feature>
</Halo>
<h1>This is where I want to dynamically inject the title for each feature on clicking corresponding feature</h1>
</div>
</div>
</template>
<script>
import Halo from '#/components/ProductHalo.vue'
import Feature from '#/components/ProductFeature.vue'
import json from '#/json/data.json'
export default {
name: 'ProductSingle',
components: {
Halo,
Feature
},
data() {
return {
products: json
}
},
computed: {
productId() {
return this.$route.params.id
}
},
methods: {
getProduct(id) {
let data = this.products
return data.filter(item => item.productId == id)
},
something(e) {
// ideally we have a method here that grabs the corresponding
//feature then displays it on the page
console.log(e.item.text)
}
}
}
</script>
My console.log call does indeed call the correct title from my data.json as seen below:
[
{
"productId": 1,
"name": "Test 1",
"image": "sample.jpg",
"features": [
{
"item": {
"text": "Something else",
"img": "sample.jpg"
}
},
{
"item": {
"text": "Turbo",
"img": "wine.jpg"
}
},
{
"item": {
"text": "Strong",
"img": "sample.jpg"
}
}
]
}
]
So it seems I can access my title based on the click of each respective item, just not sure how I can display that in an arbitrary location! Any amazing vue js'ers out there who can solve this riddle? TIA
Option1: change the child component
Instead of creating a component only just for one feature button, I would embed the whole list of features in a component with h1 where you want to show the selected feature. So, the child component will look like this:
<template>
<div>
<li v-for="(feature, key, index) in features" :key="index">
<button #click="setFeature(feature)">
<img :src="require('../assets/images/' + feature.item.img)" />
</button>
</li>
<h1>{{ clickedFeuatureText }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
clickedFeuatureText:null
}
},
methods: {
setFeature(e){
this.clickedFeuatureText = e.item.text
}
},
props: {
features: Array
}
}
</script>
And here is the parent component with corresponding changes:
<template>
<div>
<div v-for="product in getProduct(productId)" :key="product.productId">
{{ product }}
<Halo
:featuresCount="
`circle-container-` + product.features.length.toString()
"
>
<Feature :features=" product.features"></Feature>
</Halo>
</div>
</div>
</template>
<script>
import Halo from '#/components/ProductHalo.vue'
import Feature from '#/components/ProductFeature.vue'
import json from '#/json/data.json'
export default {
name: 'ProductSingle',
components: {
Halo,
Feature
},
data() {
return {
products: json
}
},
computed: {
productId() {
return this.$route.params.id
}
},
methods: {
getProduct(id) {
let data = this.products
return data.filter(item => item.productId == id)
}
}
}
</script>
Option2: create the new component for one product
Another way is to leave a child component as is, but create the new component representing one product in the list:
<template>
<div>
{{ product }}
<Halo
:featuresCount="
`circle-container-` + product.features.length.toString()
"
>
<Feature
v-for="(feature, key, index) in product.features"
:key="index"
:feature="feature"
#changeProductData="setFeatureText"
></Feature>
</Halo>
<h1>{{ clickedFeature}}</h1>
</div>
</div>
</template>
<script>
import Feature from '#/components/ProductFeature.vue'
import Halo from '#/components/ProductHalo.vue'
export default {
name: 'product',
components: {
Feature,
Halo
},
data () {
return {
clickedFeature: null
}
},
props: {
product: Object
},
methods: {
setFeatureText(e) {
this.clickedFeature = e.item.text
}
}
}
</script>
Then use the new Product component:
<template>
<div>
<product
v-for="product in getProduct(productId)"
:key="product.productId"
:product="product"
></product>
</div>
</template>
<script>
import Product from '#/components/Product.vue'
import json from '#/json/data.json'
export default {
name: 'ProductSingle',
components: {
Product
},
data() {
return {
products: json
}
},
computed: {
productId() {
return this.$route.params.id
}
},
methods: {
getProduct(id) {
let data = this.products
return data.filter(item => item.productId == id)
}
}
}
</script>

How to write a plugin that shows a modal popup using vue. Call should be made as a function()

I am trying to make a VueJS plugin that exports a global method, which when called, will popup a message with an input text field. Ideally, I want to be able to make the following call from any Vue component:
this.$disaplayMessageWithInput("Title","Body","Value");
And a popup should come on the screen.
I've tried building it but when the install() calls this.$ref., it isn't recognized:
DeleteConfirmation.vue
<template>
<b-modal size="lg" ref="deleteConfirmationModal" :title="this.title" header-bg-variant="danger" #ok="confirmDelete" #cancel="confirmCancel">
<p>
{{this.body}}
</p>
</b-modal>
</template>
<script>
export default {
data()
{
return {
title: null,
body: null,
valueCheck: null,
value: null
};
},
install(vue, options)
{
Vue.prototype.$deleteConfirmation = function(title, body, expectedValue)
{
this.title = title;
this.body = body;
this.valueCheck = expectedValue;
this.$refs.$deleteConfirmation.show()
}
},
}
</script>
app.js
import DeleteConfirmation from './components/global/DeleteConfirmation/DeleteConfirmation';
Vue.use(DeleteConfirmation);
The call I am trying to make is:
$vm0.$deleteConfirmation("title","body","val");
I get the below error at the run time:
app.js?id=c27b2799e01554aae7e1:33 Uncaught TypeError: Cannot read property 'show' of undefined
at Vue.$deleteConfirmation (app.js?id=c27b2799e01554aae7e1:33)
at <anonymous>:1:6
Vue.$deleteConfirmation # app.js?id=c27b2799e01554aae7e1:33
(anonymous) # VM1481:1
It looks like, this.$refs in DeleteConfirmation.vue is undefined.
Try to avoiding $ref with vue ( $ref is here for third party and some very special case )
$ref isn't reactive and is populate after the render ...
the best solution for me is using a event bus like this :
const EventBus = new Vue({
name: 'EventBus',
});
Vue.set(Vue.prototype, '$bus', EventBus);
And then use the event bus for calling function of your modal ...
(
this.$bus.on('event-name', callback) / this.$bus.off('event-name');
this.$bus.$emit('event-name', payload);
)
You can create a little wrapper around the bootstrap modal like mine
( exept a use the sweet-modal)
<template>
<div>
<sweet-modal
:ref="modalUid"
:title="title"
:width="width"
:class="klass"
class="modal-form"
#open="onModalOpen"
#close="onModalClose"
>
<slot />
</sweet-modal>
</div>
</template>
<script>
export default {
name: 'TModal',
props: {
eventId: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
width: {
type: String,
default: null,
},
klass: {
type: String,
default: '',
},
},
computed: {
modalUid() {
return `${this._uid}_modal`; // eslint-disable-line no-underscore-dangle
},
modalRef() {
return this.$refs[this.modalUid];
},
},
mounted() {
if (this.eventId !== null) {
this.$bus.$on([this.eventName('open'), this.eventName('close')], this.catchModalArguments);
this.$bus.$on(this.eventName('open'), this.modalRef ? this.modalRef.open : this._.noop);
this.$bus.$on(this.eventName('close'), this.modalRef ? this.modalRef.close : this._.noop);
}
},
beforeDestroy() {
if (this.eventId !== null) {
this.$off([this.eventName('open'), this.eventName('close')]);
}
},
methods: {
onModalOpen() {
this.$bus.$emit(this.eventName('opened'), ...this.modalRef.args);
},
onModalClose() {
if (this.modalRef.is_open) {
this.$bus.$emit(this.eventName('closed'), ...this.modalRef.args);
}
},
eventName(action) {
return `t-event.t-modal.${this.eventId}.${action}`;
},
catchModalArguments(...args) {
if (this.modalRef) {
this.modalRef.args = args || [];
}
},
},
};
</script>
<style lang="scss" scoped>
/deep/ .sweet-modal {
.sweet-title > h2 {
line-height: 64px !important;
margin: 0 !important;
}
}
</style>
AppModal.vue
<template>
<div class="modal-wrapper" v-if="visible">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<div class="modal-buttons">
<button class="modal-button" #click="hide">Close</button>
<button class="modal-button" #click="confirm">Confirm</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
title: '',
text: ''
}
},
methods: {
hide() {
this.visible = false;
},
}
}
</script>
Modal.js (plugin)
import AppModal from 'AppModal.vue'
const Modal = {
install(Vue, options) {
this.EventBus = new Vue()
Vue.component('app-modal', AppModal)
Vue.prototype.$modal = {
show(params) {
Modal.EventBus.$emit('show', params)
}
}
}
}
export default Modal
main.js
import Modal from 'plugin.js'
// ...
Vue.use(Modal)
App.vue
<template>
<div id="app">
// ...
<app-modal/>
</div>
</template>
This looks pretty complicated. Why don't you use a ready-to-use popup component like this one? https://www.npmjs.com/package/#soldeplata/popper-vue

How to pass initial form values to child component in Vue.js?

I'm using Vue.js. From my template I include the child component (componentB) which includes several input elements. I want to initialize those input elements from my parent template. I found a way to do this (see code below). However, I'm wondering if this is a correct way, as the articles I have read so far use different approaches (e.g. with $emit):
https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
https://zaengle.com/blog/using-v-model-on-nested-vue-components
https://alligator.io/vuejs/add-v-model-support/
Can you confirm that my code below matches the Vue.js design concepts or are there flaws?
<template>
<div>
<div class="md-layout">
<div class="md-layout-item md-size-100">
<ComponentB ref="componentB" v-model="componentB"></ComponentB>
</div>
</div>
</div>
</template>
<script>
import { ComponentB } from "#/components";
export default {
components: {
ComponentB
},
data() {
return {
componentB: {
textInputField: "my-initial-value"
}
};
},
methods: {
validate() {
return this.$refs.componentB.validate().then(res => {
this.$emit("on-validated", res);
return res;
});
}
}
};
</script>
<style></style>
Form componentB
<template>
<div>
<md-field
:class="[
{ 'md-valid': !errors.has('textInputField') && touched.textInputField },
{ 'md-form-group': true },
{ 'md-error': errors.has('textInputField') }
]"
>
<md-icon>label_important</md-icon>
<label>My text input</label>
<md-input
v-model="textInputField"
data-vv-name="textInputField"
type="text"
name="textInputField"
required
v-validate="modelValidations.textInputField"
>
</md-input>
<slide-y-down-transition>
<md-icon class="error" v-show="errors.has('textInputField')"
>close</md-icon
>
</slide-y-down-transition>
<slide-y-down-transition>
<md-icon
class="success"
v-show="!errors.has('textInputField') && touched.textInputField"
>done</md-icon
>
</slide-y-down-transition>
</md-field>
</div>
</template>
<script>
import { SlideYDownTransition } from "vue2-transitions";
export default {
name: "componentB",
props: ['value'],
components: {
SlideYDownTransition
},
computed: {
textInputField: {
get() {return this.value.textInputField},
set(textInputField) { this.$emit('input', { ...this.value, ['textInputField']: textInputField })}
}
},
data() {
return {
touched: {
textInputField: false
},
modelValidations: {
textInputField: {
required: true,
min: 5
}
}
};
},
methods: {
getError(fieldName) {
return this.errors.first(fieldName);
},
validate() {
return this.$validator.validateAll().then(res => {
return res;
});
}
},
watch: {
textInputField() {
this.touched.runnerName = true;
}
}
};
</script>
<style></style>
The simplest way to pass data to child component is to use props, which are then available in the child component and can pass the values back up to the parent.
https://v2.vuejs.org/v2/guide/components-props.html
// PARENT COMPONENT
<ComponentB :textInputField="textInputField" ...></ComponentB>
// CHILD COMPONENT
// TEMPLATE SECTION
<md-input
v-model="textInputField"
value="textInputField"
...
>
// SCRIPT SECTION
export default {
props: {
textInputField: String
}
}

How to pass data to nested child components vue js?

I get how to pass data from parent to child with props in a situation like:
<template>
<div>
<div v-for="stuff in content" v-bind:key="stuff.id">
<ul>
<li>
{{ stuff.items }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: stuff,
props: ['content'],
data () {
return {
}
}
}
</script>
And then bind the data to the component in the parent component like,
<template>
<div>
<stuff v-bind:content="stuffToPass"></stuff>
</div>
</template>
<script>
import stuff from './stuff.vue';
export default {
data () {
return {
stuffToPass: [
{id: 1, items: 'foo'},
{id: 2, items: 'bar'},
{id: 3, items: 'baz'}
]
}
},
components: {
stuff
}
}
</script>
But say I have the root component, and I want to pass data to the stuff component, like in the above, but when I have a number of other components like parent > x > y > stuff, and it's still the stuff component that will ultimately be receiving that data, I don't know how to do that.
I heard of provide/inject, but I'm not sure that's the appropriate use, or at least I couldn't get it working.
Then I tried passing props, but then I found myself trying to bind a prop to a component to pass as a prop to a child component and that doesn't sound right, so then I just re-wrote my components in the 'stuff' component, but I feel that's probably re-writing way to much code to be close to reasonable.
there are a few possibilities to pass data parent > x > y > stuff
props - applicable but you would have to pipe the data through all components...
store (vuex) - applicable but could become complicated to handle
event bus - the most flexible and direct way
below, a simple example on how to implement the event bus:
// src/services/eventBus.js
import Vue from 'vue';
export default new Vue();
the code from where you want to emit the event:
// src/components/parent.vue
<script>
import EventBus from '#/services/eventBus';
export default {
...
methods: {
eventHandler(val) {
EventBus.$emit('EVENT_NAME', val);
},
},
...
};
</script>
the code for where you want to listen for the event:
// src/components/stuff.vue
<script>
import EventBus from '#/services/eventBus';
export default {
...
mounted() {
EventBus.$on('EVENT_NAME', val => {
// do whatever you like with "val"
});
},
...
};
</script>
Use watchers or computed properties https://v2.vuejs.org/v2/guide/computed.html
const Stuff = Vue.component('stuff', {
props: ['content'],
template: `<div>
<div v-for="stuff in content" v-bind:key="stuff.id">
<ul>
<li>
{{ stuff.items }}
</li>
</ul>
</div>
</div>`
});
const Adapter = Vue.component('adapter', {
components: { Stuff },
props: ['data'],
template: `<div>
<Stuff :content="newData"/>
</div>`,
data() {
return {
newData: []
};
},
created() {
this.changeData();
},
watch: {
data: {
deep: true,
handler: function() {
this.changeData();
}
}
},
methods: {
changeData() {
this.newData = JSON.parse(JSON.stringify(this.data));
}
}
});
const app = new Vue({
el: '#app',
components: { Adapter },
data() {
return {
stuffToPass: [
{ id: 1, items: 'foo' },
{ id: 2, items: 'bar' },
{ id: 3, items: 'baz' }
]
};
},
methods: {
addItem() {
this.stuffToPass.push({ id: this.stuffToPass.length + 1, items: 'new' });
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.1/vue.js"></script>
<div id="app">
<button #click="addItem">Add</button>
<Adapter :data="stuffToPass"/>
</div>

Categories

Resources