DOM not getting updated Vue - javascript

<li
v-for="(schema) in typeSchema"
:key="schema.id"
>
<div style="display:inline-block; width:100%;">
<div style="display:flex; justify-content:space-between">
<span>{{ schema.title }}</span>
<span v-if="schema.controller">
<MdsSwitch
:checked="schema.controller.value"
:label="schema.controller.title"
#change="toggleController(schema, $event)"
/>
</span>
</div>
<div style="display:flex;flex-flow:column;place-items:flex-start;align-items:flex-start;margin-top:10px;">
<component
:is="schema.type"
v-bind="schema"
:data="data"
:is-disabled="schema.isDisabled"
#input="updateData"
/>
</div>
</div>
</li>
# toggleController(schema, event) {
if (schema.controller) {
// this.typeSchema.map(x => x).in
schema.controller.value = event;
schema.isDisabled = !event;
schema = { ...schema };
// const index = this.typeSchema.findIndex((x) => x.id === schema.id);
// console.log(index);
// this.$set(schema, "isDisabled", !event);
// this.typeSchema.splice(index, 0, schema);
}
},
When toggleController is executed it should disable the associated component, it was working earlier and I'm not sure what change I made and it stopped working, unfortunately everything is on my local so cannot refer pervious versions.
I have tried $set, splice but no luck

Related

Updating module on instant change when list is updated

Sorry for the long post, but I tried explaining things in as much detail as possible.
So as I dive deeper into JavaScript and start learning more and more about AJAX requests and other components, I've stumbled across something that I can't seem to figure out.
So below, I will explain what I'm doing and what I would like to do, and see if someone has some guidance for me.
So here is my Vue.js app:
new Vue({
name: 'o365-edit-modal',
el: '#o365-modal-edit',
data: function() {
return {
list: {},
}
},
created() {
this.fetchApplicationsMenu();
},
methods: {
fetchApplicationsMenu() {
var self = this;
wp.apiRequest( {
path: 'fh/v1/menus/applications',
method: 'GET',
}).then(menu => self.list = menu.data);
},
changed() {
const selected = this.$data.list.selected;
function get_ids(list, field) {
const output = [];
for (let i=0; i < list.length ; ++i)
output.push(list[i][field]);
return output;
}
const result = get_ids(selected, "id");
wp.apiRequest( {
path: 'fh/v1/menus/applications',
method: 'PUT',
data: {
ids: result,
},
}).then((post) => {
return post;
},
(error) => {
console.log(error);
});
},
add(x) {
this.$data.list.selected.push(...this.$data.list.available.splice(x, 1));
this.changed();
},
remove(x) {
this.$data.list.available.push(...this.$data.list.selected.splice(x, 1));
this.changed();
},
},
});
Then here is the HTML portion that I'm using to render the two columns:
<div class="column is-half-desktop is-full-mobile buttons">
<nav class="level is-mobile mb-0">
<div class="level-left">
<div class="level-item is-size-5 has-text-left">Selected</div>
</div>
<div class="level-right">
<div class="level-item">
<i class="fas fa-sort-alpha-up is-clickable"></i>
</div>
</div>
</nav>
<hr class="mt-1 mb-3">
<draggable class="list-group"
v-model="list.selected"
v-bind="dragOptions"
:list="list.selected"
:move="onMove"
#change="changed">
<button class="button is-fullwidth is-flex list-group-item o365_app_handle level is-mobile" v-for="(app, index) in list.selected" :key="app.id">
<div class="level-left">
<span class="icon" aria-hidden="true">
<img :src="app.icon_url" />
</span>
<span>{{app.name}}</span>
</div>
<div class="level-right">
<span class="icon has-text-danger is-clickable" #click="remove(index)">
<i class="fas fa-times"></i>
</span>
</div>
</button>
</draggable>
</div>
<div class="column is-half-desktop is-full-mobile buttons">
<div class="is-size-5 has-text-left">Available</div>
<hr class="mt-1 mb-3">
<draggable class="list-group"
v-model="list.available"
v-bind="dragOptions"
:list="list.available"
:move="onMove">
<button class="button is-fullwidth is-flex list-group-item o365_app_handle level is-mobile" v-for="(app, index) in list.available" :key="app.id">
<div class="level-left">
<span class="icon" aria-hidden="true">
<img :src="app.icon_url" />
</span>
<span>{{app.name}}</span>
</div>
<div class="level-right">
<span class="icon has-text-primary is-clickable" #click="add(index)">
<i class="fas fa-plus"></i>
</span>
</div>
</button>
</draggable>
</div>
That outputs the following items, and all works great. See the video display below of each component working as needed. This all works great! I'm calling the changed() method on add and remove which grabs all the IDs and stores them in the DB via an endpoint.
The Problem:
Now I have the following dropdown menu, which depends on the fh/v1/menus/applications endpoint to pull in all the items as shown below:
As you can see below, when I open the dropdown, it has three apps, when I open the cog wheel and remove one of the apps and it saves it but the dropdown doesn't get automatically updated, I have to refresh the page and then I will see the updates.
Does anyone know how to fetch the new items without a refresh?
Here is the HTML and the JS for the dropdown piece:
HTML: As you can see in there, I have data-source="applications" which pulls in the items inside the init_menu as shown in the JS.
<div class="dropdown-menu" id="dropdown-o365" role="menu">
<div class="dropdown-content">
<div class="container is-fluid px-4 pb-4">
<?php if ($application = Applications::init()): ?>
<div class="columns">
<div class="dropdown-item column is-full has-text-centered is-size-6">
<div class="level is-mobile">
<div class="level-left">
<?= $application->get_name() ?>
</div>
<div class="level-right">
<a class="navbar-item modal-element icon" id="o365-apps-cogwheel" data-target="o365-modal-edit" aria-haspopup="true">
<i class="fa fa-cog"></i>
</a>
</div>
</div>
</div>
</div>
<div class="columns is-multiline" data-source="applications"></div>
<?php else: ?>
<div class="columns">
<div class="column is-full">
No applications present.
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
Then here is the JavaScript. I initilize the method inside DOMContentLoaded using init_menu('applications');:
function init_menu(paths)
{
paths.forEach(path => {
const target = document.querySelector('[data-source=' + path + ']');
if (target) {
wp.api.loadPromise.done(function () {
const Menus = wp.api.models.Post.extend({
url: wpApiSettings.root + 'fh/v1/menus/' + path,
});
const menus = new Menus();
menus.fetch().then(posts => {
// This returns the data object.
const data = posts.data;
let post_list;
// Check if it's an array and see if selected is empty otherwise show available.
if (Array.isArray(data.selected) && data.selected.length !== 0) {
post_list = data.selected;
} else {
post_list = data.available;
}
post_list.forEach(function (post) {
switch(path) {
case 'applications':
target.appendChild(create_apps_dom_tree(post));
break;
default:
console.log('Path route is invalid.');
break;
}
})
})
})
}
});
}
function create_apps_dom_tree(post) {
const {
icon_url,
url,
name,
} = post
const container = document.createElement('div');
container.className = 'column is-one-third is-flex py-0';
const anchor = document.createElement('a');
anchor.href = url;
anchor.className = 'dropdown-item px-2 is-flex is-align-items-center';
const figure = document.createElement('figure');
figure.className = 'image is-32x32 is-flex';
const img = document.createElement('img');
img.src = icon_url;
const span = document.createElement('span');
span.className = 'pl-2';
span.textContent = name;
figure.appendChild(img);
anchor.append(figure, span);
container.appendChild(anchor);
return container;
}
If anyone has some guidance or an answer on how to pull in live data from the database on the fly, that would be amazing.
Basically, I need my data-source: to automatically grab the items when my vue/db request is sent so I don't have to refresh the page.
Inside my Vue app, I have the following method:
fetchApplicationsMenu() {
var self = this;
wp.apiRequest( {
path: 'fh/v1/menus/applications',
method: 'GET',
}).then(menu => self.list = menu.data);
},
which calls a GET request and then stores the data inside the return { list: {} }.
A quick fix might be to just invoke init_menu() from the component's beforeDestroy() hook, called when the dialog closes. You might choose to do it from changed() instead if the dropdown is still accessible with this dialog open.
new Vue({
// option 1:
beforeDestroy() {
init_menu('applications');
},
// option 2:
methods: {
changed() {
init_menu('applications');
}
}
})
Alternative: You already know what the final application list is in changed(), so you could update the dropdown with the new list from that method.
function update_menu(path, post_list) {
const target = document.querySelector('[data-source=' + path + ']');
// remove all existing children
Array.from(target.childNodes).forEach(x => x.remove());
post_list.forEach(post => target.appendChild(create_apps_dom_tree(post)))
}
new Vue({
methods: {
changed() {
update_menu('applications', this.$data.available);
}
}
})

Vue js: load more data button not working properly

In my Vue.js code below I'm trying to add a Show More button to my data coming from API so initially it should show 10 data and whenever clicked load more 10 and so on. I tried answer from:
Load more button in vuejs
but it's not working since I'm looping over an array it gives me the error below can't read property of question title. Is there a way to do it?
<div class="search-askbutton">
<b-row>
<div class="search-wrapper">
<input
type="text"
v-model="search"
placeholder="Search something..."
class="fas fa-search"
/>
</div>
<div class="container vue">
<div v-for="commentIndex in commentsToShow">
<div v-if="commentIndex <= commentsToShow">
<ul
class="container-question"
v-for="(question, index) in filteredList"
:key="index"
>
<div>{{question[commentIndex - 1].questionTitle}} says:</div>
<hr />
</ul>
</div>
</div>
<button #click="commentsToShow += 10">show more</button>
</div>
<script>
export default {
data() {
return { commentsToShow: 10,
search: '',
questions: [],}
},
computed: {
filteredList() {
return this.questions.filter((question) => {
return (
question.questionTitle
.toLowerCase()
.includes(this.search.toLowerCase()) ||
question.owner.username
.toLowerCase()
.includes(this.search.toLowerCase()) ||
question.questionTitle
.toUpperCase()
.includes(this.search.toUpperCase()) ||
question.owner.username
.toUpperCase()
.includes(this.search.toUpperCase())
);
});
},
},
mounted: function() {
questionService.getAllQuestions().then((response) => {
this.questions = response.data.response;}
}
</script>
The problem is your subtraction
<div>{{question[commentIndex - 1].questionTitle}} says:</div>
If commentIndex = 0 then you'll be saying 0-1 = -1 therefore it will not find the -1 index.
You could replace this line
<div v-if="commentIndex <= commentsToShow">
So that it can run only if the index is greater than 0
<div v-if="commentIndex > 0">
1)
v-for returns what's inside an array, not the array itself.
<div>{{question.questionTitle}} says:</div>
2)
also, you can remove the v-for loop.
note:- your reference question is also uses this way.
<div v-for="commentIndex in commentsToShow">
<div v-if="commentIndex <= commentsToShow">
<ul class="container-question">
<div>{{filteredList[commentIndex - 1].questionTitle}} says:</div>
<hr />
</ul>
</div>
</div>

Deleting object in Vue.js

I've got a problem and need help. Im trying to teach in Vue.js. I'm doing a quiz and i want to put here a button with deleting a question. I tried this:
deleteQuestion(index)
{
this.questions.splice(index, 1);
}
but it will delete always only first question although when i want to delete second question. Any help?
Full Code:
HTML:
<div class="question" v-for="question in questions">
<!-- Znění otázky -->
<h2>{{ question.question }}</h2><button #click="deleteQuestion" class="doprava"><img src="criss.png"/></button>
<!-- Odpovědi -->
<label v-for="answer in question.answers" class="answer" :class="{ 'answer-correct':answer.correct, 'answer-false':answer.false }">
<input type="checkbox" :value="answer.id" v-model="question.selected"> {{ answer.answer }}
</label>
</div>
<hr>
<button #click="onSubmit()">onSubmit</button>
JS:
var vm = new Vue({
el: "#app",
data: {
questions: questions,
result: 0
},
methods: {
onSubmit() {
this.result = 0
this.questions.forEach(question => {
question.answers.forEach(answer => {
answer.correct = question.correct.includes(answer.id);
answer.false = question.false.includes(answer.id);
});
});
},
deleteQuestion(index)
{
this.questions.splice(index, 1);
}
}
});
You haven't passed an index to your deleteQuestion method. Try this in your template instead:
<div class="question" v-for="(question, index) in questions">
<h2>{{ question.question }}</h2>
<button #click="deleteQuestion(index)" class="doprava">
<img src="criss.png"/>
</button>
etc.
However a better option might be to give each question an id property and delete it by filtering agains that id instead. This is because you can't guarantee that the order/index of questions in the template will be consistent on each re-render. This could be done like so:
<button #click="deleteQuestion(question.id)">
Then in your method:
deleteQuestion(id) {
this.questions = this.questions.filter(q => q.id !== id);
}

Bind element inside a for loop Vue not working properly

In the following Vue Component I want to loop through dwarfs array. And as long as I am in the current component, everything is fine (TEST) and also all the following properties are correct.
Currenct_Component.vue :
<template>
<div>
<h2>Stamm: {{ tribeName }}</h2>
<div class="card-container">
<div class="card" style="width: 18rem;" v-for="dwarf in dwarfs" :key="dwarf.name">
<!-- TEST -->
<p>{{dwarf}}</p>
<!-- CHILD COMPONENT -->
<app-modal
:showModal="showModal"
:targetDwarf="dwarf"
#close="showModal = false"
#weaponAdded="notifyApp"
/>
<!-- <img class="card-img-top" src="" alt="Card image cap">-->
<div class="card-body">
<h3 class="card-title" ref="dwarfName">{{ dwarf.name }}</h3>
<hr>
<ul class="dwarf-details">
<li><strong>Alter:</strong> {{ dwarf.age }}</li>
<li><strong>Waffen:</strong>
<ul v-for="weapon in dwarf.weapons">
<li><span>Name: {{ weapon.name }} | Magischer Wert: {{ weapon.magicValue }}</span></li>
</ul>
</li>
<li><strong>Powerfactor:</strong> {{ dwarf.weapons.map(weapon => weapon.magicValue).reduce((accumulator, currentValue) => accumulator + currentValue) }}</li>
</ul>
<button class="card-button" #click="showModal = true"><span class="plus-sign">+</span> Waffe</button>
</div>
</div>
</div>
<button id="backBtn" #click="onClick">Zurück</button>
</div>
</template>
<script>
import Modal from './NewWeaponModal.vue';
export default {
data() {
return {
showModal: false,
}
},
components: { appModal : Modal },
props: ['tribeName', 'dwarfs'],
methods: {
onClick() {
this.$emit('backBtn')
},
notifyApp() {
this.showModal = false;
this.$emit('weaponAdded');
}
},
}
</script>
But when I bind the element dwarf to the Child Component <app-modal/> it changes to the next dwarf in the array dwarfs (TEST) - (So as the result when i add a new weapon in the modal-form it gets added to the second dwarf...):
Child_Component.vue :
<template>
<div>
<div class="myModal" v-show="showModal">
<div class="modal-content">
<span #click="$emit('close')" class="close">×</span>
<h3>Neue Waffe</h3>
<!-- TEST -->
<p>{{ targetDwarf }}</p>
<form>
<input
type="text"
placeholder="Name..."
v-model="weaponName"
required
/>
<input
type="number"
placeholder="Magischer Wert..."
v-model="magicValue"
required
/>
<button #click.prevent="onClick">bestätigen</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
weaponName: '',
magicValue: '',
}
},
props: ['showModal', 'targetDwarf'],
methods: {
onClick() {
if(this.weaponName !== '' &&
Number.isInteger(+this.magicValue)) {
let newData = {...this.dwarf};
newData['weapons'] = [
...this.dwarf['weapons'],
{
"name": this.weaponName,
"magicValue": Number.parseInt(this.magicValue)
},
];
this.$http.post("https://localhost:5019/api", newData)
.then(data => data.text())
.then(text => console.log(text))
.catch(err => console.log(err));
this.$emit('weaponAdded');
} else {
alert('You should fill all fields validly')
}
},
}
}
</script>
It looks like you have the <app-modal/> component inside of the v-for="dwarf in dwarfs" loop, but then the control for showing all of the modal components created by that loop is just in one variable: showModal. So when showModal is true, the modal will show each of the dwarfs, and I'm guessing the second dwarf's modal is just covering up the first one's.
To fix this, you could move the <app-modal /> outside of that v-for loop, so there's only one instance on the page, then as part of the logic that shows the modal, populate the props of the modal with the correct dwarf's info.
Something like this:
<div class="card-container">
<div class="card" v-for="dwarf in dwarfs" :key="dwarf.name">
<p>{{dwarf}}</p>
<div class="card-body">
<button
class="card-button"
#click="() => setModalDwarf(dwarf)"
>
Waffe
</button>
</div>
</div>
<!-- Move outside of v-for loop -->
<app-modal
:showModal="!!modalDwarfId"
:targetDwarf="modalDwarfId"
#close="modalDwarfId = null"
#weaponAdded="onDwarfWeaponAdd"
/>
</div>
export default {
//....
data: () => ({
modalDwarfId: null,
)},
methods: {
setModalDwarf(dwarf) {
this.modalDwarfId = drawf.id;
},
onDwarfWeaponAdd() {
//...
}
},
}
You could then grab the correct dwarf data within the modal, from the ID passed as a prop, or pass in more granular data to the modal so it's more "dumb", which is the better practice so that the component isn't dependent on a specific data structure. Hope that helps
Courtesy of #Joe Dalton's answer, a bit alternated for my case:
<div class="card" style="width: 18rem;" v-for="dwarf in dwarfs" :key="dwarf.name">
...
<button class="card-button" #click="setModalDwarf(dwarf)"><span class="plus-sign">+</span> Waffe</button>
<div>
<app-modal
:showModal="showModal"
:targetDwarf="currentDwarf"
#close="showModal = false"
#weaponAdded="notifyApp"
/>
<script>
import Modal from './NewWeaponModal.vue';
export default {
data() {
return {
showModal: false,
currentDwarf: null,
}
},
components: { appModal : Modal },
props: ['tribeName', 'dwarfs'],
methods: {
setModalDwarf(dwarf) {
this.currentDwarf = dwarf;
this.showModal = true;
},
...
}
</script>

Vue.js can't find element using querySelector

I am trying to create a chat style form. So a user inputs their data and then uses the button within my template with the class of continue-btn.
As you can see when the continue-btn is pressed it uses the nextStep method which adds 1 to the counter data property.
Within my template I then use v-if="counter >= 1" to display the next section of the chat dialog and input field.
I am then trying to use scrollTop to automatically scroll the page to the new section with the id of #conversation__tram-1. I originally tried running this block of code just after the counter had been given a value of 1:
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
This didn't work though because I'm guessing the #conversation__tram-1 element hadn't been added to the DOM yet.
So for the sake of testing I tried wrapping it in a timeout function:
setTimeout(function(){
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
}, 3000);
However I am left with this error when trying this:
Uncaught TypeError: Cannot read property 'querySelector' of undefined
Here is my whole single vue file:
<template>
<div id="conversation-app">
<!-- <div v-for="item in items">
{{ item.text }}
</div> -->
<div class="conversation__track">
<div id="conversation__tram-0">
<div class="conversation__item agent">
<img src="/assets/cdn.annuityadvicecentre.dev/images/theme-f/michael-chat-agent.jpg" class="conversation__item-prof-img" alt="Michael Chat Agent" />
<div class="conversation__item-content">
<p>
Hello my name is {{ agent }}, we'll compare the whole annuity market to bring you back the best annuity rates from the top providers for you. Let's get started, what's your name?
</p>
</div>
</div>
<div class="conversation__item customer" id="title-fullname">
<div class="conversation__item-content">
<p>
Hi {{ agent }}, my name is...
</p>
<div class="row">
<div class="col-4">
<select id="title" class="field-title" name="payload[title]"><option value="mr">Mr</option><option value="mrs">Mrs</option><option value="miss">Miss</option><option value="ms">Ms</option></select>
</div>
<div class="col-8">
<input v-model="customerName" id="full_name" class="field-full_name" name="payload[full_name]" type="text">
</div>
</div>
</div>
</div>
</div>
<transition name="fade">
<div id="conversation__tram-1" v-if="counter >= 1">
<div class="conversation__item agent">
<img src="/assets/cdn.annuityadvicecentre.dev/images/theme-f/michael-chat-agent.jpg" class="conversation__item-prof-img" alt="Michael Chat Agent" />
<div class="conversation__item-content">
<p>
Thanks {{ firstName }}, nice to meet you. To process your instant quote please can I have your Pension Value?
</p>
</div>
</div>
<div class="conversation__item customer">
<div class="conversation__item-content">
<p>
Sure, my pension value is...
</p>
<input id="pension_value" class="field-pension_value" placeholder="£" pattern="\d*" name="payload[pension_value]" type="number">
<div class="error-wrap error_pension_value is-hidden" data-format="<div class="error-text">:message</div>"></div>
</div>
</div>
</div>
</transition>
<div id="conversation__buttons">
<button type="button" class="continue-btn"
v-on:click="nextStep"
>Continue <i class="fa fa-chevron-right" aria-hidden="true"></i></button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'conversation-app',
data () {
return {
agent: 'Brick',
counter: 0,
customerName: '',
}
},
methods: {
nextStep: function() {
this.counter += 1;
setTimeout(function(){
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
}, 3000);
},
},
computed: {
firstName() {
return this.customerName.split(' ')[0];
}
}
}
</script>
Any idea why this isn't working? Thanks.
This is a good time to use arrow functions, as they preserve the context of this.
nextStep: function() {
this.counter += 1;
setTimeout(() => {
const container = this.$el.querySelector("#conversation__tram-" + this.counter);
container.scrollTop = container.scrollHeight;
}, 3000);
Altenatively, instead of the timeout you can use Vue.nextTick which is a more technically-correct way of doing this.
nextStep: function () {
this.counter += 1
this.$nextTick(() => { ... })

Categories

Resources