Trying to pull data from Vue components in single file components - vue.js

This is my first time experimenting with Vue.js so it's altogether possible I'm missing something very obvious.
Basically, I have a component that calculates the number of boxes needed for a certain quantity of a printed piece (I work in the print industry).
If needed, I have a button to create an additional component if the printed piece has multiple parts.
I'd like to have a reactive way to update the total number of boxes needed for all parts, but I just can't seem to get there.
Here's a link to my Gitlab repo with the code: https://gitlab.com/dsross/printutils
Any help would be appreciated.
I'm also using Browserify to write the build.js and build.css files referenced in index.html.
Here are my files, in case no one wants to look at the repo:
App.vue
<template>
<div id="app">
<div>
</div>
<div>
<calculator v-for="(part, index) in parts" :key="index"></calculator>
<br />
<br />
<div class="card shadow-sm">
<div class="card-body">
<button type="button" class="btn" #click="addPart()">Add Part</button>
<button type="button" class="btn" #click="totalBoxes">Total Boxes</button>
<span>Box Total (all parts): </span><span id="grandtotal"></span>
</div>
</div>
</div>
</div>
</template>
<script>
// import Hello from './components/Hello.vue'
import Calculator from './components/Calculator.vue'
export default {
name: 'app',
components: {
Calculator
},
methods: {
addPart: function () {
console.log("Adding part");
this.parts.push(Calculator);
},
totalBoxes: function () {
console.log("totalBoxes called");
let totalBoxes = 0;
let partTotals = document.querySelectorAll("#partBoxTotal");
for (var i = 0; i < partTotals.length; i++) {
totalBoxes += parseInt(partTotals[i].innerHTML);
}
this.totalBoxCount = totalBoxes;
document.getElementById("grandtotal").innerHTML = totalBoxes;
}
},
data: function () {
return {
parts: [Calculator],
totalBoxCount: 0
}
},
}
</script>
Calculator.vue
<template>
<div class="card shadow-sm" id="boxCalculator">
<div class="card-body">
<form>
<div class="form-group">
<p>Paper:
<select class="custom-select" v-model="paperWeight">
<option v-for="(mweight, paper) in mweights" :key="mweight" v-bind:value="paper">{{paper}}</option>
</select>
</p>
<br />
<br />
<p>Final Width:
<input class="form-control" type="text" v-model="finalWidth" id="finalWidth" value="">
</p>
<p>Final Height:
<input type="text" class="form-control" v-model="finalHeight" id="finalHeight" value="">
</p>
<p>Sheets Per Unit:
<input type="text" class="form-control" v-model="numberOfSheets" id="numberOfSheets" name="numberOfSheets"
value="">
</p>
<p>Quantity:
<input type="text" class="form-control" v-model="quantity" id="quantity" name='quantity'>
</p>
<p>Stitched:
<input type="checkbox" v-model="stitched" name="stitched" id="stitched" value="">
</p>
</div>
</form>
<div class="card">
<div class="card-body">
<div id='results'>
<p id="partWeightTotal">Part Total Weight: {{ totalWeight }}</p>
<p><span>Part Box Total: </span><span id="partBoxTotal">{{ boxQuantity }}</span></p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {
mWeights,
stitchedMultiplier,
maxArea
} from "../constants.js"
module.exports = {
data: function () {
return {
paperWeight: this.selected,
paperType: "",
finalWidth: "",
finalHeight: "",
numberOfSheets: "",
quantity: "",
stitched: "",
boxes: "",
mweights: mWeights
}
},
computed: {
squareInches: function () {
return this.finalHeight * this.finalWidth;
},
squareInchWeight: function () {
let mWeight = mWeights[`${this.paperWeight}`];
return (mWeight / 1000) / maxArea;
},
totalWeight: function () {
return ((this.squareInches * this.squareInchWeight) * this.numberOfSheets) * this.quantity;
},
boxQuantity: function () {
let boxes = this.totalWeight / 35;
if (this.stitched) {
this.boxes = Math.ceil(boxes * stitchedMultiplier);
// this.$root.$emit('box-change', this.boxes);
return this.boxes
} else {
this.boxes = Math.ceil(boxes);
// this.$root.$emit('box-change', this.boxes);
return Math.ceil(this.boxes);
};
},
},
}
</script>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>boxcalculator2</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="../favicon.png">
<title>Box Calculator</title>
<!-- Bootstrap core CSS -->
<link href="dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="dist/sticky-footer.css" rel="stylesheet">
<link rel="stylesheet" href="dist/build.css">
</head>
<body>
<div class="container">
<div class='row'>
<div class='col'>
<div id="app"></div>
</div>
</div>
</div>
<script src="dist/build.js"></script>
</body>
</html>

If I understand correctly, you'd like the App's total box count to be updated automatically whenever the individual Calculators determine their box counts. One way to do this is to emit an event from Calculator when its box count changes, which could be monitored with a watcher.
There are a couple issues we'll address below:
It doesn't make sense (and is inefficient) to store Calculator -- a single-file-component definition -- in this.parts[]. Instead, it could store meaningful data points, such as Calculator's output.
Instead of DOM manipulation (i.e., querying the DOM for an element to get/set its value), opt for modeling the data in Vue, and using interpolation in the template. This lets Vue automatically display the updated value in the target element. It also obviates the element ID assignments (assuming they were used exclusively for DOM manipulation), simplifying the template for improved readability.
Storing Calculator output
In App, we must use this.parts[] to track the result of each part's calculation (which we'll capture below). We'll define each array element (i.e., a "part") to be:
{
boxes: 0 // box count for this part
}
This definition allows a computed property (which we'll define later), based on .boxes, to be reactive.
So, in addPart() and the data option:
// App.vue
export default {
// ...
methods: {
addPart() {
this.parts.push({ boxes: 0 });
}
},
data() {
return {
parts: [{ boxes: 0 }]
}
}
}
Notifying App of Calculator output
Typically, parents pass data to children via props, and children communicate data to parents with events. Alternatives include using a state management library, such as Vuex, but for the sake of simplicity, we'll use events here.
In Calculator, we want to notify the parent (App) of changes to the boxes value, so we'll add a watcher that emits an event (e.g., called boxes-changed) whenever boxes changes:
// Calculator.vue
export default {
//...
watch: {
boxes(value) {
this.$emit('boxes-changed', value);
}
}
}
In App, we'll listen to the boxes-changed event, and copy the event detail's value to the current part's boxes variable, where part is the current array element of parts[] being iterated.
// App.vue
<calculator v-for="(part, index) in parts" #boxes-changed="part.boxes = $event" :key="index"></calculator>
Breakdown of #boxes-changed="part.boxes = $event":
#boxes-changed="..." - listen to boxes-changed event emitted from <calculator>
part.boxes = $event - set part.boxes to value of event detail
Making totalBoxCount reactive
With the changes above, we have the tools needed to make App's totalBoxCount reactive:
Change totalBoxCount into a computed property that sums up the .boxes fields of this.parts[]. This property will be computed automatically whenever array elements of this.parts[] changes.
// App.vue
export default {
computed: {
totalBoxCount() {
// Note: Alternatively, use a simple for-loop to sum .boxes
return this.parts
.filter(p => p.boxes && !Number.isNaN(p.boxes) // get only parts that have a positive `.boxes` value
.map(p => p.boxes) // map object array into an integer array of `.boxes` values
.reduce((p,c) => p + c, 0); // sum all array elements
},
},
data() {
return {
parts: [],
// totalBoxCount: 0 // CHANGED INTO COMPTUED PROP ABOVE
}
}
}
In App's template, use string interpolation to display totalBoxCount:
<!--
<span>Box Total (all parts): </span><span id="grandtotal"></span>
--> <!-- DON'T DO THIS -->
<span>Box Total (all parts): {{totalBoxCount}}</span>
We might as well remove the Total Boxes button (previously used to manually trigger a calculation) from the template:
<!--
<button type="button" class="btn" #click="totalBoxes">Total Boxes</button>
--> <!-- DELETE -->
and its associated click-handler:
// App.vue
export default {
methods: {
// totalBoxes: function() { /* .. */ } // DELETE THIS
}
}
demo

Related

Page not re-rendering when data is updated -- Vue.js

Can't get why the app is not re-rendering when data is updated using method, can anyone see what's the issue? thanks!
On clicking the button, selectedPage is updating using the changePage method, but nothing is changing on the page (in the Vue dev tool I can see that selectedPage is changing correctly)
var app = new Vue({
el: '#app',
data: {
showMain: false,
isReader: true,
selectedPage: [0,0],
books: [
{
id: 0,
name: 'איה פלוטו',
image: 'https://upload.wikimedia.org/wikipedia/he/f/f3/%D7%90%D7%99%D7%94_%D7%A4%D7%9C%D7%95%D7%98%D7%95.jpg',
pageList: [
{id: 0, text: [',פלוטו כלבלב מקיבוץ מגידו','יש לו הכל, חלב ועצם','זה טוב ויפה, אבל בעצם','נמאס לו לשבת כך לבדו'], image: './images/wheres_pluto/page_1.jpeg'},
{id: 1, text: ['lived a princes in a big castle'], image: './images/wheres_pluto/page_2.jpeg'},
{id: 2, text: ['the END'], image: './images/wheres_pluto/page_3.jpeg'}
]
},
{
id: 1,
name: 'ספר שני',
pageList:[]
}
],
},
methods: {
changePage: function(event){
if(event == 'next'){
this.selectedPage[1] = this.selectedPage[1] + 1
} else {
this.selectedPage[1] = this.selectedPage[1] - 1
}
console.log(this.selectedPage)
}
}
})
The html:
<html>
<head>
<link rel="stylesheet" type="text/css" href="css/style.css">
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght#100;300;400;500&display=swap" rel="stylesheet">
</head>
<body>
<div id='app'>
<!-- book selection page -->
<div class='page app-page' v-show="showMain">
<div class='book-list' v-for="book in books" :book='book.name'>{{book.name}}
<img :src='book.image' />
</div>
</div>
<!-- Book -->
<div class="book" v-for="book in books" v-if='book.id == selectedPage[0]' :key=book.id>
<div class="book-page" v-for="(page,index) in book.pageList" v-if='book.id == selectedPage[0] && book.pageList[index].id == selectedPage[1]'>
<!-- Video component -->
<div class="video">VIDEO HERE</div>
<!-- Reader's page -->
<div class="page reader-page" v-if="isReader" :key = page.id>
<div class="main-text" v-for='(lines,lineIndex) in book.pageList[index].text'>
<span>{{page.text[lineIndex]}}</span>
</div>
</div>
<!-- Kid's page -->
<div class="page kid-page" v-else>
<div class="video">VIDEO HERE</div>
<div class="img-container" :style="{ backgroundImage: 'url(' + page.image + ')' }">
</div>
</div>
<!-- controllers -->
<div class="controllers" v-if=isReader>
<button class="next" v-on:click='changePage("next")'>הבא</button>
<button class="prev" v-on:click='changePage("prev")'>קודם</button>
</div>
</div>
</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="js/main.js"></script>
</html>
DTal, A couple of things to consider here. I noticed that you're using selectedPage for both the page number and the page ID. It may be simpler to map both of these to two separate variables.
The other thing to consider is that you may also want to bind what book is selected as well. Vuejs won't update the internals because it's already done the v-for loop. If you bind one of the divs (using v-bind:) to a book, and keeping track of which book is selected in the data() area then it may have the behavior you want.
There were a lot of things to be fixed in the template to make your code run but the main thing missing which was not making it run was that (copying directly from Vue documentation)
Vue cannot detect the following changes to an array:
1. When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
2. When you modify the length of the array, e.g. vm.items.length = newLength
Change detection runs only for property mutations
Which can be achieved using
vm.$set(vm.items, indexOfItem, newValue)
which in your case would be
if (event === "next") {
this.$set(this.selectedPage, 1, this.selectedPage[1] + 1);
} else {
this.$set(this.selectedPage, 1, this.selectedPage[1] - 1);
}
I definitely feel that you can maintain the page numbers in a more efficient way(but that is a different concern)

Trouble with special characters in Vue input

I have a simple form in Vue but need the value of an input to contain less than and greater than symbols along with a few pieces of data. This is causing Vue to break on compile since it's not valid. How can I make it work?
Here's an example:
<input type="hidden" id="DAT" name="DAT" :value="
<USERID>item.user_id</USERID>
"/>
new Vue({
data() {
return {
item: {
user_id: "123456"
}
}
},
template: '<input type="text" id="DAT" name="DAT" :value="`<USERID>${item.user_id}</USERID>`"/>'
}).$mount("#app")
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Binding vue components to class name

Alright so I am trying to bind this vue components to a class name so it triggers on every element that has this class but what happens is that it only works with the first element and not with other ones
<div class="__comment_post">
<textarea></textarea>
<input type="submit" v-on:click="submitComment" /> <!-- submit comment being only triggered on this one -->
</div>
<div class="__comment_post">
<textarea></textarea>
<input type="submit" v-on:click="submitComment" />
</div>
<div class="__comment_post">
<textarea></textarea>
<input type="submit" v-on:click="submitComment" />
</div>
As you can see above, I've got 3 divs with class __comment_post so naturally submitComment should be bound to all these 3 divs but what happens is that submitComment is being triggered only on the first one
var app = new Vue({
el:".__comment_post",
data: {
comment: ""
},
methods: {
submitComment: function() {
console.log("Test");
}
}
});
Here is a little example you and others can follow in order to bind vue instance to class names.
Lets say, you would like to bind Vue to multiple existing <div class="comment"> element in HTML.
HTML:
<div class="comment" data-id="1">
<div>
<div class="comment" data-id="2">
<div>
Now, you can try the following logic/code to your example.
JS:
var comments = {
"1": {"content": "Comment 1"},
"2": {"content": "Comment 2"}
}
$('.comment').each(function () {
var $el = $(this)
var id = $el.attr('data-id')
var data = comments[id]
new Vue({
el: this,
data: data,
template: '<div class="comment">{{ content }}<div>'
})
})
I hope this will answer your question :)
The vue instance is mounted on the first found DOM element with the css selector passed to the el option. So the rest two div have no vue instances mounted on them.
So wrap your divs with a wrapper div and mount the vue instance on that wrapper
<div id="app">
<div class="__comment_post">
<textarea></textarea>
<input type="submit" v-on:click="submitComment" /> <!-- submit comment being only triggered on this one -->
</div>
<div class="__comment_post">
<textarea></textarea>
<input type="submit" v-on:click="submitComment" />
</div>
<div class="__comment_post">
<textarea></textarea>
<input type="submit" v-on:click="submitComment" />
</div>
script
var app = new Vue({
el:"#app",
data: {
comment: ""
},
methods: {
submitComment: function() {
console.log("Test");
}
}
});

Validation Error messages on form load

About the issue
I am using Laravel 5.6.7 with vue.js. vee-validate is being used for validation
When the form loads, it shows validation error messages. User did not even click the submit button. Below is the screenshot.
Code
<template>
<div>
<form role="form">
<input v-validate data-vv-rules="required" type="text"
v-model="UpdateForm.First_Name">
<p v-if="errors.has('First Name')">{{ errors.first('First Name') }}</p>
<button type="button">
Update Profile
</button>
</form>
</div>
</template>
<script>
export default {
data() {
return {
UpdateForm: {
First_Name: ''
}
}
},
created() {
this.GetProfile();
},
methods: {
GetProfile() {
axios.post("some api url", {}).then(response => {
this.UpdateForm.First_Name = response.data.Data.First_Name;
});
}
}
}
</script>
Could I get rid of validation error messages on form load?
This is not the expected behavior. For initial validating you need to inform it with v-validate.initial.
Maybe you are defining this to happen when declaring v-validate or in other place.
Vue.use(VeeValidate);
new Vue({
el: '#demo'
})
.is-danger{
color: red;
}
<script src="https://unpkg.com/vue"></script>
<script src="https://cdn.jsdelivr.net/npm/vee-validate#latest/dist/vee-validate.js"></script>
<div id="demo">
<label>This one needs touching</label>
<input type="text" name="name" v-validate="'required'">
<div v-show="errors.has('name')" class="is-danger">Errors: {{ errors.first('name') }}</div>
<br/>
<label>This one does not need touching</label>
<input name="name2" v-validate.initial="'required'" type="text">
<div v-show="errors.has('name2')" class="is-danger">{{ errors.first('name2') }}</div>
</div>
Changed
this.editForm.First_Name = Data.User.First_Name;
to
if(Data.User.First_Name != null && Data.User.First_Name != "") {
this.editForm.First_Name = Data.User.First_Name;
}
and validation is working fine now. Basically the variable is not initialized.

Dropzone inside a html form with other form fields not working

I want to add a dropzone inside an existing form but it doesn't seem to work.
When I view the console I get error throw new Error("No URL provided"). When I click upload I get no preview either - all I get is a normal file input.
<link href="../dropzone.css" rel="stylesheet">
<form action="/" enctype="multipart/form-data" method="POST">
<input type="text" id ="Username" name ="Username" />
<div class="dropzone" id="my-dropzone" name="mainFileUploader">
<div class="fallback">
<input name="file" type="file" />
</div>
</div>
<div>
<button type="submit" id="submit"> upload </button>
</div>
</form>
<script src="../jquery.min.js"></script>
<script src="../dropzone.js"></script>
<script>
$("my-dropzone").dropzone({
url: "/file/upload",
paramName: "file"
});
</script>
No url provided error is because $("my-dropzone") is wrong instead it must be $('#mydropzone')
dropzone along with other form, yes this is very much possible, you have to post the data using the URL provided in the dropzone not in the form action. That means all your form data along with the files uploaded shall be posted back to the url provided for the dropzone. A simple untested solution is as below;
<link href="../dropzone.css" rel="stylesheet">
<form action="/" enctype="multipart/form-data" method="POST">
<input type="text" id ="Username" name ="Username" />
<div class="dropzone" id="my-dropzone" name="mainFileUploader">
<div id="previewDiv></div>
<div class="fallback">
<input name="file" type="file" />
</div>
</div>
<div>
<button type="submit" id="submitForm"> upload </button>
</div>
</form>
<script src="../jquery.min.js"></script>
<script src="../dropzone.js"></script>
<script>
$("#mydropzone").dropzone({
url: "/<controller>/action/" ,
autoProcessQueue: false,
uploadMultiple: true, //if you want more than a file to be uploaded
addRemoveLinks:true,
maxFiles: 10,
previewsContainer: '#previewDiv',
init: function () {
var submitButton = document.querySelector("#submitForm");
var wrapperThis = this;
submitButton.addEventListener("click", function () {
wrapperThis.processQueue();
});
this.on("addedfile", function (file) {
// Create the remove button
var removeButton = Dropzone.createElement("<button class="yourclass"> Remove File</button>");
// Listen to the click event
removeButton.addEventListener("click", function (e) {
// Make sure the button click doesn't submit the form:
e.preventDefault();
e.stopPropagation();
// Remove the file preview.
wrapperThis.removeFile(file);
});
file.previewElement.appendChild(removeButton);
});
// Also if you want to post any additional data, you can do it here
this.on('sending', function (data, xhr, formData) {
formData.append("PKId", $("#PKId").val());
});
this.on("maxfilesexceeded", function(file) {
alert('max files exceeded');
// handle max+1 file.
});
}
});
</script>
The script where you initialize dropzone can be inside $document.ready or wrap it as a function and call when you want to initialize it.
Happy coding!!