Update nested parent classes in Vue.js - javascript

I'm trying to add a class to a parent container each time an "advanced" link is clicked.
So with jQuery I would just write something like..
$(this).closest('.row').addClass('overlay');
or
$(this).closest('section').addClass('overlay');
But it seems to be getting a little complex with Vue to just add a class to the parent container of the item that is clicked. I'm sure there is a more simple way to go about it.
Here is an example of my code.
<div id="app">
<section v-bind:class="{ overlay: i == sectionActive && rowActive == null }" v-for="(section, i) in sections">
Advanced
<div class="advanced-fields" v-bind:class="{ overlay: i == sectionActive && rowActive == null }">
<fieldset>
<label>
ID
<input type="text" name="section[id]" v-model="section.id">
</label>
</fieldset>
<fieldset>
<label>
Class
<input type="text" name="section[css_class]" v-model="section.css_class">
</label>
</fieldset>
</div>
<div class="row" v-bind:class="{ overlay: i == sectionActive && row_i == rowActive }" v-for="(row, row_i) in section.rows">
Advanced
<div class="advanced-fields" v-bind:class="{ overlay: i == sectionActive && row_i == rowActive }">
<fieldset>
<label>ID</label>
<input type="text" name="" v-model="row.id">
</fieldset>
<fieldset>
<label>CSS Class</label>
<input type="text" name="" v-model="row.css_class">
</fieldset>
</div>
</div>
</section>
<pre>{{ $data }}</pre>
</div>
<script>
new Vue({
el: "#app",
data: {
"sections": [{
"id": "section-1",
"css_class": "",
"rows": [{
"id": "",
"css_class": ""
}, {
"id": "",
"css_class": ""
}]
}, {
"id": "section-2",
"css_class": '',
"rows": [{
"id": "",
"css_class": ""
}]
}],
sectionActive: null,
rowActive: null,
columnActive: null
},
methods: {
toggleAdvanced: function(index) {
this.sectionActive = this.sectionActive === index ? null : index;
this.rowActive = null;
this.columnActive = null;
},
toggleRowAdvanced: function(section, row) {
var sectionIndex = this.sections.indexOf(section);
var rowIndex = section.rows.indexOf(row);
this.sectionActive = this.sectionActive === sectionIndex ? null : sectionIndex;
this.rowActive = this.rowActive === rowIndex ? null : rowIndex;
}
}
});
</script>
I need to do the same thing for columns but as you can see, it is getting too overly complicated. Any ideas on how to simplify this?
I know it would be easier to add a data attribute to each row, but I am saving the hash to a database and don't want to add in unnecessary data just for a UI toggle.
https://jsfiddle.net/ferne97/4jbutbkz/

I took a different approach and built several re-usable components. This removes all the complicated logic that you are putting into your Vue.
Vue.component("expand-link",{
template:`Advanced`,
data(){
return {
expanded: false
}
},
methods:{
toggle(){
this.expanded = !this.expanded
this.$emit('toggled', this.expanded)
}
}
})
Vue.component("expanded-fields",{
props:["details", "expanded"],
template:`
<div class="advanced-fields" :class="{overlay: expanded}">
<fieldset>
<label>
ID
<input type="text" name="section[id]" v-model="details.id">
</label>
</fieldset>
<fieldset>
<label>
Class
<input type="text" name="section[css_class]" v-model="details.css_class">
</label>
</fieldset>
</div>
`
})
Vue.component("expandable-section", {
props:["section"],
template:`
<section>
<expand-link #toggled="onToggle"></expand-link>
<expanded-fields :details="section" :expanded="expanded"></expanded-fields>
<expandable-row v-for="row in section.rows" :key="row" :row="row"></expandable-row>
</section>
`,
data(){
return {
expanded: false
}
},
methods:{
onToggle(val){
this.expanded = val
}
}
})
Vue.component("expandable-row",{
props:["row"],
template:`
<div class="row">
<h3>Row</h3>
<expand-link #toggled="onToggle"></expand-link>
<expanded-fields :details="row" :expanded="expanded"></expanded-fields>
</div>
`,
data(){
return {
expanded: false
}
},
methods:{
onToggle(val){
this.expanded = val
}
}
})
And the template simply becomes
<div id="app">
<expandable-section v-for="section in sections" :key="section" :section="section"></expandable-section>
<pre>{{ $data }}</pre>
</div>
Here is your fiddle updated.

Related

Conditionally render a div with vue.js

I am trying to build a quiz which when the user chooses a selection and submits the answer the notification div displays whether choice is correct or not. I have got this logic working however what is happening is the div is disappearing each time the next question is answered. I want to keep the previous question result on the screen and lock the user out of answering the question again and show the correct answer.
Here is what I have so far:
<div
class="container"
v-for="options in quiz"
v-bind:key="options.quizId"
>
<h3>{{ options.question }}</h3>
<div
class="control"
>
<label class="radio">
<input
type="radio"
:value="options.op1"
v-model="selectedAnswer"
/>
{{ options.op1 }}
</label>
</div>
<div class="control">
<label class="radio">
<input
type="radio"
:value="options.op2"
v-model="selectedAnswer"
/>
{{ options.op2 }}
</label>
</div>
<div class="control">
<label class="radio">
<input
type="radio"
:value="options.op3"
v-model="selectedAnswer"
/>
{{ options.op3 }}
</label>
</div>
<div class="control">
<label class="radio">
<input
type="radio"
:value="options.op4"
v-model="selectedAnswer"
/>
{{ options.op4 }}
</label>
</div>
<div class="control mt-4" >
<button class="button is-link" #click="call(options.id)">
Submit
</button>
</div>
<br />
<template v-if="options.id === quizId">
<template v-if="quizResult == 'correct'">
<template v-if="correctAnswer">
<div class="notification is-success mt-4">
Correct. Well done!
</div>
</template>
<br />
</template>
<template v-if="quizResult == 'incorrect'">
<div class="notification is-danger mt-4">
Incorrect. Please try again.
</div>
<br />
</template>
</template>
</div>
</div>
</div>
</div> <br>
</section>
</div>
</div>
<!-- <hr /> -->
</template>
<script>
import axios from "axios";
export default {
data() {
return {
course: {},
lessons: [],
comments: [],
activeLesson: null,
errors: [],
showModalFlag1: false,
okPressed1: false,
quiz: {},
questionIndex: 0,
loading: true,
quizId: null,
selectedAnswer: "",
quizResult: null,
correctAnswer: false,
userScore: {
username: '',
lesson_id: '',
lesson_score: '',
},
images: [
{
photo: "",
},
],
comment: {
name: "",
content: "",
},
};
},
methods: {
call(id){
this.submitQuiz(id);
console.log('checkid#call',id)
},
submitQuiz(e) {
console.log('check-id#submit', e)
const quizArray = this.quiz;
const choice = this.selectedAnswer;
console.log('chosen: ', choice)
const result = quizArray.filter( obj => obj.op1 === choice || obj.op2 === choice ||
obj.op3 === choice || obj.op4 === choice)[0];
console.log('result',result.id);
for (const prop in result) {
if ( result.hasOwnProperty(prop) ) {
if (result.id == e) {
this.quizId = result.id;
if (choice == result.answer) {
this.quizResult = "correct";
this.correctAnswer = true;
}
if(choice !== result.answer){
this.quizId = result.id;
this.quizResult = "incorrect";
}
}
}
}
},
You could have another div or in your notification div to display last answered question.
You can either have a new object that gets overwritten each time you answer correctly or track the index of your loop and use the value of index - 1 to get last answered question. Add a check if index !== 0.
To get index of your loop in html do: v-for="(options, ) in quiz
I'm seeing conditions with inner conditions for the correct answer notification. So if your quizId is the next one, the outer condition already fails.
It's likely you have an array with questions and thus have an index for the current question. You could save a property a property on that index that shows if the question is correct, so your condition can be extended to also look back for the previous question
Something like this (pseudo code)
<template v-if="showCorrectMsg">
Good!
...
and then have a computed showCorrectMsg method something like this (pseudo code):
if ( questions[currentIndex].quizResult == 'correct' ||
( currentIndex > 0 && questions[currentIndex - 1].quizResult == 'correct' )
) return true;
return false;
So for this to work you will have to store that flag on each answer given (again, pseudo code):
questions[currentIndex].quizResult = [ theAnswer == theRightAnswer ] ? 'correct' : 'wrong';

Dynamic generated form with checkbox in Vue

I have dynamic generated form in Vue. Every loop have v-model. Everything work fine. But when I use checkboxes v-model work for all loops not on one like in input type text. Can You help me solved this problem? Below Vue code:
<fieldset>
<div class="form-row mb-2" v-for="input, index in journal" :key="index">
<div class="col-auto">
<label for="date">Data</label>
<Datepicker v-model="input.date" input-class="form-control" :input-attr="{id: 'date', name: 'date'}" style="width: 100%;" />
</div>
<div class="col-md-2">
<label for="timeStart">Od</label>
<Datepicker type="time" v-model="input.timeStart" format="HH:mm" input-class="form-control" :input-attr="{id: 'timeStart', name: 'timeStart'}" style="width: 100%;" />
</div>
<div class="col-md-2">
<label for="timeEnd">Do</label>
<Datepicker type="time" v-model="input.timeEnd" format="HH:mm" input-class="form-control" :input-attr="{id: 'timeEnd', name: 'timeEnd'}" style="width: 100%;" />
</div>
<div class="col-md-2">
<label for="players">Lista obecności</label>
<div class="form-check" v-for="item in input.players">
<input v-model="item.checked" type="checkbox" class="form-check-input" :id="'id-'+item.id+'set'+index">
<label class="form-check-label" :for="'id-'+item.id+'set'+index">{{ item.fullName }}</label>
</div>
</div>
<div class="col-auto">
<label for="description">Opis</label>
<textarea v-model="input.description" class="form-control" rows="7" id="description" placeholder="Opis"></textarea>
</div>
<div class="col-auto" #click="addInput" v-show="index == journal.length-1 && journal.length < 16">
<ButtonVue style="margin-top: 30px;" title="Dodaj" type="button" cancelWidth="true" color="btn-success"><i class="fas fa-plus"></i></ButtonVue>
</div>
<div class="col-auto align-self-start" #click="removeInput(index)" v-show="index || ( !index && journal.length > 1)">
<ButtonVue style="margin-top: 30px;" title="Usuń" type="button" cancelWidth="true" color="btn-danger"><i class="fas fa-minus"></i></ButtonVue>
</div>
</div>
</fieldset>
 
data() {
return {
contact: [],
journal: [{
date: "",
timeStart: "",
timeEnd: "",
players: "",
description: ""
}],
contacts: [],
}
},
Methods:
Method for creating dynamic form
addInput() {
this.journal.push({
date: "",
timeStart: "",
timeEnd: "",
players: this.contact,
description: ""
});
},
And here is the method which gets players from contacts
getContacts() {
this.pageLoader = true;
this.$http.get('/pkpar/get-contacts')
.then(({
data
}) => {
this.contacts = data.contacts;
for(let i=0; i<this.contacts.length; i++)
{
this.contact.push({'id': this.contacts[i]['id'], 'fullName' :
this.contacts[i]['fullName'], 'checked': true});
}
this.journal[0].players = this.contact;
this.pageLoader = false;
})
.catch(error => {
console.log(error);
});
},
Your addInput method creates and pushes new object into journal array, but each object created this way has a players property which references same array (this.contact)
The Difference Between Values and References in JavaScript
Easiest (but not most optimal) way to handle this is to create a copy of the array and objects inside for each new journal:
addInput() {
this.journal.push({
date: "",
timeStart: "",
timeEnd: "",
players: this.contact.map((player) => ({ ...player })),
description: ""
});
},

How to append input elements dynamically in vue

I'm trying to append new input fields based on a condition, I will describe the workflow to help you understand
First stage is to press this button to implement 2 functions(1 is to move to other fieldset in the staged form, second function is to append the inputs:
<input type="button" name="secondBtn" class="next action-button" value="Next" id="secondBtn" #click="nextPlusappend"/>
nextPlusappend:
nextPlusappend() {
this.isNextClicked();
this.appendFields();
}
appendFields:
//this.platform initllized as 'one' so the condition is true.
if(this.platform === 'one'){
this.inputFields.push({
Username: '',
Password: '',
AffiliateID: '',
CI: '',
GI: '',
})
}
And I want to append all the fields from the function to this fieldset:
<div v-if="currentPage === 2">
<fieldset id="fieldset3" v-for="(param, index) in inputFields" :key="index">
<h2 class="fs-title">API Credentials</h2>
<h3 class="fs-subtitle">Step 3- Add any parameter for the integration</h3>
<input v-model="param.Username" type="text" name="`inputFields[${index}[Username]]`" placeholder="userName">
<input type="button" name="previous" class="previous action-button" value="Previous" #click="isPreviousClicked"/>
<input type="submit" name="submit" class="submit action-button" value="Create a Ticket" id="excel"/>
</fieldset>
</div>
How can I append this without hard code all the input fields as I did here:?
<input v-model="param.Username" type="text" name="`inputFields[${index}[Username]]`" placeholder="userName">
This is designated to be dynamic, what do i mean?
I mean that if the this.platform is equal to "one" there will be a unique fields, and if this.platform equal to "two" for example there will be other unique fields.
Don't think like "pushing a form field", rather think like "adding a new item to the dataset" (and of course, its displayed UI is a form field).
Let me give an example:
Vue.component("FormField", {
props: ["label", "value"],
computed: {
val: {
get() {
return this.value
},
set(val) {
this.$emit("update:value", val)
}
},
},
methods: {
handleClickAdd() {
this.$emit("click-add-field")
}
},
template: `
<div>
<label>
{{ label }}: <input type="text" v-model="val" />
</label>
<button
#click="handleClickAdd"
>
+ ADD
</button>
</div>
`,
})
new Vue({
el: "#app",
data() {
return {
formFields: [{
label: "Field 1",
value: null,
}],
}
},
methods: {
handleClickAddField() {
const item = {
label: `Field ${ this.formFields.length + 1 }`,
value: null,
}
this.formFields = [...this.formFields, item]
},
},
template: `
<div
class="container"
>
<div
class="col"
>
<h4>FIELDS:</h4>
<hr>
<form-field
v-for="(field, i) in formFields"
:key="i"
:label="field.label"
:value.sync="field.value"
#click-add-field="handleClickAddField"
/>
</div>
<div
class="col"
>
<h4>FIELD VALUES:</h4>
<hr>
<div
v-for="(field, i) in formFields"
:key="i"
>{{ field.label }}: {{ field.value }}</div>
</div>
</div>
`,
})
.container {
display: flex;
}
.col {
padding: 0 8px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
You can see, that on ADD I just added a new item in the formFields - the values are bound in the template to a child-component, that handles the actual representation of the fields.
On the right side of the snippet, you can see another benefit of decoupling data from UI: I created another representation of the same data source - that immediately reacts to any changes!

Make calculation from v-for in vue js

i'm trying to make simple calculation from data given in vuejs. The calculation is just going well. But, when i started to make sum from calculation total from it, it keeps returning NaN value.
Here is the current code of it:
<div class="row" v-for="fields in data" :key="field.id_m_all_ded">
<div class="col-md-5">
<div class="form-group">
<label>{{ field.name }}</label>
<input type="field.type" class="form-control" #change="calculated(field)" v-model="field.value" />
</div>
</div>
<div class="col-md-7">
<label>Total Deductions</label>
<input type="number" class="form-control" v-model="field.total" disabled />
</div>
</div>
<div class="row">
<div class="col-md-5">
<label>Total Allowance</label>
<input type="number" class="form-control" v-model="grandTotal" id="total" disabled />
</div>
</div>
I retrieve the data from my API, and it saved in fields[]:
data() {
return {
model: {
nik: "",
name: "",
basic_salary: "",
period: "",
},
modals: {
modal: false,
},
fields: [{
total: ''
}],
totalData: 11,
page: 1,
mode: "",
};
},
And here's the function:
computed: {
grandTotal: function() {
let temp = 0
for (var i = 0; i < this.fields.length; i++) {
temp = temp + Number(this.fields[i].total)
console.log(temp)
}
console.log(temp)
return temp;
}
},
methods: {
calculated(field){
field.total = 4000000 * (field.value / 100)
console.log(field.total)
}
}
For addtional info, i get the data from the api, but it won't calculate automatic. Therefore, i tried it with manual input first to calculate the value.
What code should i fix from it, because i have no clue.
Thanks

AngularJS: two way data binding not working in nested ng-repeat

I am new to AngularJS. I am using nested ng-repeat to generate multiple forms. And form data is json which is dynamic. So the problem is Data binding for TEXT, TEXTAREA and SELECT input is not working.
JSFiddle: http://jsfiddle.net/gTc5v/10/
HTML:
<div ng-controller="ctrl">
<div ng-repeat="form in forms">
<h2>{{form.name}}</h2>
<div ng-repeat="field in form.form_fields">
<div ng-If="showElement(field,'RADIO')">
<div>
<h3>{{field.caption}}</h3>
<span ng-repeat="option in field.optionValues">
<input ng-model="field.answer" type="radio" value="{{option}}" >{{option}}
<br/></input>
</span>
</div>
</div>
<div ng-If="showElement(field,'TEXT')">
<input ng-model="field.answer" type="text" placeholder="{{field.caption}}" />
</div>
<div ng-If="showElement(field,'PARAGRAPH_TEXT')">
<textarea ng-model="field.answer" placeholder="{{field.caption}}"></textarea>
</div>
<div ng-If="showElement(field,'LIST')">
<div>
<h3>{{field.caption}}</h3>
<select ng-model="field.answer">
<option ng-repeat="option in field.optionValues">{{option}}</option>
</select>
</div>
</div>
<div ng-If="showElement(field,'CHECKBOX')">
<div>
<h3>{{field.caption}}</h3>
<span ng-repeat="option in field.optionValues">
<input ng-checked="field.answers.indexOf(option) != -1" ng-click="toggleCheck(field.answers,option)" type="checkbox">{{option}}
<br/></input>
</span>
</div>
</div>
</div>
<br/>
<div ng-repeat="field in form.form_fields">
{{field.caption}} : {{field.answer}}{{field.answers}}
</div>
<br/>
</div>
AangularJS:
angular.module('myApp', []).directive('ngIf', function () {
return {
link: function (scope, element, attrs) {
if (scope.$eval(attrs.ngIf)) {
// remove '<div ng-if...></div>'
element.replaceWith(element.children());
} else {
element.replaceWith(' ');
}
}
};
});
function ctrl($scope) {
$scope.fields = [{
"caption": "Gender",
"questionType": "RADIO",
"optionValues": ["Male", "Female"],
"fieldPriority": "INCLUDE"
}, {
"caption": "City",
"questionType": "TEXT",
"optionValues": "",
"fieldPriority": "INCLUDE"
}, {
"caption": "Address",
"questionType": "PARAGRAPH_TEXT",
"optionValues": "",
"fieldPriority": "INCLUDE"
}, {
"caption": "Nationality",
"questionType": "LIST",
"optionValues": ["Indian", "American"],
"fieldPriority": "INCLUDE"
}, {
"caption": "Tea/Coffee",
"questionType": "CHECKBOX",
"optionValues": ["Tea", "Coffee"],
"fieldPriority": "INCLUDE"
}];
angular.forEach($scope.fields, function(field) {
if(field.questionType == 'CHECKBOX'){
field.answers = [];
} else{
field.answer = "";
}
});
$scope.forms = [{"name":"Form 1","form_fields" : angular.copy($scope.fields)},{"name":"Form 2","form_fields" : angular.copy($scope.fields)}];
$scope.showElement = function (field, type) {
return field.questionType == type;
};
$scope.toggleCheck = function (answer, option) {
if (answer.indexOf(option) === -1) {
answer.push(option);
} else {
answer.splice(answer.indexOf(option), 1);
}
};
}
thanks
Try to remove element.replaceWith below in your directive link function.
element.replaceWith(element.children());
You don't need to call replaceWith() in directive because directive has wrapped the template content and it will be included once compiled.
Here is a workable jsfiddle demo
Do you really need your own ng-If directive? Your example works fine if you use the built-in ng-if.
Note that in either case you need to write it as a lower-case ng-if in your HTML.
Update:
jsfiddle

Categories