How to correctly get object length in JS - laravel

This data is coming from the controller, and I have it in the mounted and in the data like so
data() {
return {
forum: [],
}
},
mounted() {
if (this.$page.forum) {
this.forum = this.$page.forum;
}
}
I have this data, it's very long so I won't be posting it all but there should be 13 comments total.
{
"id":1,
"theme":"werwer",
"description":"werwer",
"user_id":1,
"anonymous":0,
"start_date":"2019-12-01 06:00:00",
"end_date":"2019-12-20 12:00:00",
"created_at":"2019-12-04 12:00:50",
"updated_at":"2019-12-09 08:15:47",
"user":{
"id":1,
"name":"sadmin",
"card":"1111",
"scard":"123",
"user_type_id":1,
"email":"sadmin#gmail.com",
"created_at":"2019-12-04 12:00:14",
"updated_at":"2019-12-04 12:00:14"
},
"comments":[
{
"id":5,
"user_id":1,
"discussion_forum_id":1,
"parent_id":3,
"comment":"Este comentario ha sido eliminado.",
"comment_time":"2019-12-09 08:58:10",
"deleted":1,
"created_at":"2019-12-04 12:09:19",
"updated_at":"2019-12-09 08:58:10",
"user":{
"id":1,
"name":"sadmin",
"card":"1111",
"scard":"123",
"user_type_id":1,
"email":"sadmin#gmail.com",
"created_at":"2019-12-04 12:00:14",
"updated_at":"2019-12-04 12:00:14"
},
"replies":[
{
"id":6,
"user_id":1,
"discussion_forum_id":1,
"parent_id":5,
"comment":"reply to reply",
"comment_time":"2019-12-04 12:15:19",
"deleted":0,
"created_at":"2019-12-04 12:15:19",
"updated_at":"2019-12-04 12:15:19",
"user":{
"id":1,
"name":"sadmin",
"card":"1111",
"scard":"123",
"user_type_id":1,
"email":"sadmin#gmail.com",
"created_at":"2019-12-04 12:00:14",
"updated_at":"2019-12-04 12:00:14"
}
}
]
},
]
}
I want to get the comments length so I tried {{forum.comments.length}} and am using it like this
<div v-if="forum.comments.length === 0">
<el-card>
<p>
No comments!
</p>
</el-card>
</div>
<div v-else>
<div v-for="comment in forum.comments">
<el-card>
//show comments
</el-card>
</div>
</div>
How ever I get these errors
Error in render: "TypeError: Cannot read property 'length' of undefined"
Cannot read property 'length' of undefined at <div v-if="forum.comments.length === 0">
The code itself works, and does the expected but still gets those 2 errors always. What is the correct way to do this and get rid of these errors?

Assuming the explanation is your data is loaded asynchronously and not available at component rendering.
The solution is to wrap your template into a whole check for forum.comments existence to avoid rendering the block before data is loaded. For instance :
<template v-if="forum && Array.isArray(forum.comments)">
<div v-if="forum.comments.length === 0">
<el-card>
<p>
No comments!
</p>
</el-card>
</div>
<div v-else>
<div v-for="comment in forum.comments">
<el-card>
//show comments
</el-card>
</div>
</div>
</template>

Related

Vue.js 3 Uncaught (in promise) TypeError: Cannot convert undefined or null to object

So, I'm trying to fetch my laravel api and already run php artisan serve. After that the error came out and I'm looking to find solution here but I have no idea what's going on.
The error shown in the console:
Uncaught (in promise) TypeError: Cannot convert undefined or null to object
The API:
{
"message": "List trash and category order by time",
"data": [
{
"id": 1,
"trash_id": 1,
"category_id": 2,
"created_at": "2022-01-01T12:41:43.000000Z",
"updated_at": "2022-01-01T12:41:43.000000Z",
"garbage": {
"id": 1,
"name": "Buku",
"weight": 1,
"created_at": "2022-01-01T12:41:19.000000Z",
"updated_at": "2022-01-01T12:41:19.000000Z"
},
"categories": {
"id": 2,
"name": "Kertas",
"price": 1800
}
}]
}
Index.vue script:
<script>
import axios from "axios";
import { onMounted, ref } from "vue";
export default {
setup() {
// reactive state
let all_trash = ref([]);
onMounted(() => {
// get data from api endpoint
axios
.get("http://127.0.0.1:8000/api/trash")
.then((result) => {
all_trash.value = result.data;
})
.catch((err) => {
console.log(err.response);
});
});
return {
all_trash,
};
},
};
</script>
Index.vue HTML:
<div v-for="(trash, index) in all_trash.data" :key="index">
<div class="card rounded shadow mb-3">
<div class="card-body">
<div class="mx-3">
<h5 class="mb-4">{{ trash.garbage.name }}</h5>
<p>
{{ trash.categories.name }}
<span class="float-end">
<button class="btn" style="color: red">Hapus</button>
</span>
</p>
</div>
</div>
</div>
</div>
Just add a condition that not to render the loop until data is not set. The data from API end point is not available on the initial dom rendered and there is not data in all_trash.data hence the error. Just add a v-if on top of the div and hopefully it should work.
<div v-if=" all_trash.data">
<div v-for="(trash, index) in all_trash.data" :key="index">
<div class="card rounded shadow mb-3">
<div class="card-body">
<div class="mx-3">
<h5 class="mb-4">{{ trash.garbage.name }}</h5>
<p>
{{ trash.categories.name }}
<span class="float-end">
<button class="btn" style="color: red">Hapus</button>
</span>
</p>
</div>
</div>
</div>
</div>

How to show Array of errors in Vue.js ? Backend Validation with Laravel

I have some complex data and I want to show the validation error array data in vue file but I can not do it because I have got some data that has an index and showing like contacts.0.name: ["...."].
Please share your opinion how I can show the error.
vue file
<template>
<div>
<form enctype="multipart/form-data" #submit.prevent="handleSubmit">
<div v-for="(contact, index) in contacts" :key="index" class="row">
<div class="col col-md-3">
<div class="form-group mb-4">
<label for="personName">Contact Person Name</label>
<input
id="personName"
v-model="contact.name"
type="text"
class="form-control"
/>
<small> Want to show here the error ? </small
>
</div>
</div>
<!-- Add or Remove button -->
<div class="col col-md-12 text-right">
<div class="row ml-4">
<div v-show="index == contacts.length - 1">
<button
class="btn btn-warning mb-2 mr-2 btn-rounded"
#click.prevent="add"
>
Add More
</button>
</div>
<div v-show="index || (!index && contacts.length > 1)">
<button
class="btn btn-danger mb-2 mr-2 btn-rounded"
#click.prevent="remove"
>
Remove
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</template>
<script>
export default {
data() {
return {
contacts: [
{
name: "",
},
],
errors: [],
};
},
methods: {
handleSubmit() {
let data = new FormData();
data.append("contacts", JSON.stringify(this.contacts));
Request.POST_REQ(data, "/add-institute")
.then(() => {
alert("success");
})
.catch((err) => {
this.errors = err.response.data.errors;
});
},
add() {
this.contacts.push({
name: "",
email: "",
phone: "",
alternate_phone: "",
});
},
remove(index) {
this.contacts.splice(index, 1);
},
},
};
</script>
controller file
public function add_institute(Request $request) {
$request['contacts'] = json_decode($request['contacts'], true);
$request->validate([
'contacts.*.name'=> 'unique:institute_contact_people|distinct',
]);
...rest of code of insert
return response()->json("Success...");
}
Getting Error Response data
errors: {
contacts.0.name: ["The contacts.0.name has already been taken.", "The contacts.0.name field has a duplicate value."]
0: "The contacts.0.name has already been taken."
contacts.1.name: ["The contacts.1.name has already been taken.", "The contacts.1.name field has a duplicate value."]
0: "The contacts.1.name has already been taken."
}
Okay, so your error data is basically an object with array of errors in it.
Pretty much like this
errors: {
'contacts.0.name': [
'The contacts.0.name has already been taken.',
'The contacts.0.name field has a duplicate value.',
],
'contacts.1.name': [
'The contacts.1.name has already been taken.',
'The contacts.1.name field has a duplicate value.',
],
},
For me, it will be better if you could achieve something like this as an error response (an array of objects with errors array in it)
betterErrors: [
{
location: 'contact.0.name',
errors: [
'The contacts.0.name has already been taken.',
'The contacts.0.name field has a duplicate value.',
],
},
{
location: 'contact.1.name',
errors: [
'The contacts.1.name has already been taken.',
'The contacts.1.name field has a duplicate value.',
],
},
],
For me, as of right now, it feels wrong but you can achieve a display of your errors with something like this
<template>
<div>
<div v-for="(error, key) in errors" :key="key">
<hr />
<span v-for="(errorItem, innerKey) in error" :key="innerKey" style="margin-top: 2rem">
{{ errorItem }}
</span>
</div>
</div>
</template>
<script>
export default {
data() {
return {
errors: {
'contacts.0.name': [
'The contacts.0.name has already been taken.',
'The contacts.0.name field has a duplicate value.',
],
'contacts.1.name': [
'The contacts.1.name has already been taken.',
'The contacts.1.name field has a duplicate value.',
],
},
}
},
}
</script>
PS: having a :key with an array looping index is really bad tbh. That's why I do recommend a location field in your error response.
in your controller
$request->validate([
'ClinicName' => 'required|string|min:200',
'Branches.*.BranchName'=>'required|string|min:200'
]);
in your vue3 file, to access the errors which will have keys such as,
'Branches.0.BranchName'
then you can access the above error with for loop similar to this
<p v-if="form.errors['Branches.' + counter + '.BranchName']"
class="mt-2 text-sm text-red-600 dark:text-red-500">
{{ form.errors["Branches." + counter + ".BranchName"] }}
</p>
here the counter can be any counter starting from 0.

How to bind formControlName value with *ngFor in Angular

Response Json from back-end
{
"INCOME": {
"TOURIST": [{
"vehicleNumber": "TN 47 RG 4567",
"details": [{
"id": 01,
"amount": 100
},{
"id": 02,
"amount": 200
}]
},
{
"vehicleNumber": "TN 47 RG 9876",
"details": [{
"id": 03,
"amount": 300
},{
"id": 04,
"amount": 400
}]
}
]
}
}
is assigned to this.tesTourist = this.objectValue.INCOME.TOURIST;
component.html
<form [formGroup]="accountsForm" autocomplete="off">
<div class="row">
<div *ngFor="let obj of tesTourist">
<div *ngFor="let item of obj.details">
<label class="text-left"> {{obj.vehicleNumber}} :</label>
<input type="text" formControlname="incomeTourist" value="{{item.amount}}">
</div>
</div>
</div>
</form>
component.ts
this.accountsForm = this.fb.group({
incomeTourist:[],
});
Let as take obj.details.length is 2. It displays 2 input box in page with the value amount binded(item.amount). While changing the amount in any one of the input field that could not bind the amount in incomeTourist. It constantly shows null as below.
Result of <pre><code>{{accountsForm?.value | json}}</code></pre>
{
"incomeTourist": null
}
You form structure should be like below.
this.accountsForm = this.fb.group({
testTourist: this.fb.array([]),
});
After that create 2 method to create testTourist and incomeTourist form array.
createTestTourist() {
return this.formBuilder.group({
details: this.fb.array([])
});
}
createDetails() {
return this.formBuilder.group({
amount: [],
vehicleNumber: []
});
}
And after that loop over the testTourist object in ngOnInt method.
this.testTourists.forEach(testTourist => {
const fb = this.createTestTourist();
testTourist.details.forEach((detail, i) => {
const cfb = this.createDetails();
cfb.get('amount').setValue(detail.amount);
cfb.get('vehicleNumber').setValue(detail.vehicleNumber);
(fb.controls.details as FormArray).push(cfb);
});
(this.accountsForm.get('testTourist') as FormArray).push(fb);
});
after this in html you should write below code.
<form [formGroup]="accountsForm" autocomplete="off">
<div class="row">
<div *ngFor="let obj of testTourists; let i= index" formArrayName="testTourist">
<ng-container [formGroupName]="i">
<div *ngFor="let item of obj.details; let j= index" formArrayName="details">
<ng-container [formGroupName]="j">
<label class="text-left"> {{item.vehicleNumber}} :</label>
<input type="text" formControlName="amount">
</ng-container>
</div>
</ng-container>
</div>
</div>
</form>
After this if you do any changes it will reflect in form object. You can console the form object.
console.log(this.accountsForm.value)
you will get expect same object structure as you are providing. And you will get updated values.

Toggle form in nested v-for loop in VueJS

I have a list of nested comments. Under each comment, I'd like to add a "reply" button that, when click, show a reply form.
For now, everytime I click a "reply" button, it shows the form. But the thing is, I'd like to show only one form on the whole page. So basically, when I click on "reply" it should close the other form alreay opened and open a new one under the right comment.
Edit :
So I was able to make some slight progress. Now I'm able to only have one active form opening on each level of depth in the nested loop. Obviously, what I'm trying to do now is to only have one at all.
What I did was emitting an event from the child component and handle everything in the parent component. The thing is, it would work great in a non-nested comment list but not so much in my case...
Here is the new code:
In the parentComponent, I have a handleSelected method as such:
handleSelected (id) {
if(this.selectedItem === id)
this.selectedItem = null;
else
this.selectedItem = id;
},
And my childComponent:
<template>
<div v-if="comment">
<div v-bind:style=" iAmSelected ? 'background: red;' : 'background: none;' ">
<p>{{ comment.author.name }}<br />{{ comment.created_at }}</p>
<p>{{ comment.content }}</p>
<button class="button" #click="toggle(comment.id)">Répondre</button>
<button class="button" #click="remove(comment.id)">Supprimer</button>
<div v-show="iAmSelected">
<form #submit.prevent="submit">
<div class="form-group">
<label for="comment">Votre réponse</label>
<textarea class="form-control" name="comment" id="comment" rows="5" v-model="fields.comment"></textarea>
<div v-if="errors && errors.comment" class="text-danger">{{ errors.comment[0] }}</div>
</div>
<button type="submit" class="btn btn-primary">Envoyer</button>
<div v-if="success" class="alert alert-success mt-3">
Votre réponse a bien été envoyée !
</div>
</form>
</div>
</div>
<div v-if="comment.hasReply">
<div style="margin-left: 30px;">
<comment v-for="comment in comments"
:key="comment.id"
:comment="comment" #remove-comment="remove"
:is-selected="selectedItem" #selected="handleSelected($event)">
</comment>
</div>
</div>
</div>
</template>
<script>
import comment from './CommentItem'
export default {
name: 'comment',
props: {
isSelected: Number,
comment: {
required: true,
type: Object,
}
},
data () {
return {
comments: null,
fields: {},
errors: {},
success: false,
loaded: true,
selectedItem: null,
}
},
computed: {
iAmSelected () {
return this.isSelected === this.comment.id;
}
},
methods: {
remove(id) {
this.$emit('remove-comment', id)
},
toggle(id) {
this.$emit('selected', id);
},
handleSelected(id) {
if(this.selectedItem === id)
this.selectedItem = null;
else
this.selectedItem = id;
},
},
mounted(){
if (this.comment.hasReply) {
axios.get('/comment/replies/' + this.comment.id)
.then(response => {
this.comments = response.data
})
}
}
}
</script>
Thanks in advance for your help!

How to empty input fields from a pop-up window after submitting - Vue - laravel?

My page exist of a table where I can add new rows. If you want to add a new row a pop-up window appear where the new values can be added.
This new data is then saved to the database after submitting. If I again want to add a new row the input fields, they should be cleared.
The method I use, is working but isn't very clear.
Note: My code shows only a part of the input fields, to make it more clear. My pop-up window actually contains 20 input fields.
I would like to clear them all at once instead of clearing them one by one (like I am doing now).
Because I am already doing this for defining the v-model, pushing the new data to the database directly on the page and via post axios request.
Is there a cleaner way to do this?
Thanks for any input you could give me.
This is my code:
html part
<div class="col-2 md-2">
<button class="btn btn-success btn-sx" #click="showModal('add')">Add New</button>
<b-modal :ref="'add'" hide-footer title="Add new" size="lg">
<div class="row" >
<div class="col-4">
<b-form-group label="Category">
<b-form-input type="text" v-model="newCategory"></b-form-input>
</b-form-group>
</div>
<div class="col-4">
<b-form-group label="Name">
<b-form-input type="text" v-model="newName" placeholder="cd4"></b-form-input>
</b-form-group>
</div>
<div class="col-4">
<b-form-group label="Amount">
<b-form-input type="number" v-model="newAmount" ></b-form-input>
</b-form-group>
</div>
</div>
<div class="row" >
<div class="col-8">
</div>
<div class="col-4">
<div class="mt-2">
<b-button #click="hideModal('add')">Close</b-button>
<b-button #click="storeAntibody(antibodies.item)" variant="success">Save New Antibody</b-button>
</div>
</div>
</div>
</b-modal>
</div>
js part
<script>
import { async } from 'q';
export default {
props: ['speciedata'],
data() {
return {
species: this.speciedata,
newCategory: '',
newName: '',
newAmount:'',
}
},
computed: {
},
mounted () {
},
methods: {
showModal: function() {
this.$refs["add"].show()
},
hideModal: function(id, expId) {
this.$refs['add'].hide()
},
addRow: function(){
this.species.push({
category: this.newCategory,
name: this.newName,
amount: this.newAmount,
})
},
storeSpecie: async function() {
axios.post('/specie/store', {
category: this.newCategory,
name: this.newName,
amount: this.newAmount,
})
.then(this.addRow())
// Clear input
.then(
this.newName = '',
this.newCategory = '',
this.newAmount = '',
)
.then(this.hideModal('add'))
},
}
}
</script>
in your data of vuejs app , you have to set one object for displaying modal data like modalData then to reset data you can create one function and set default value by checking type of value using loop through modalData object keys
var app = new Vue({
el: '#app',
data: {
message:"Hi there",
modalData:{
key1:"value1",
key2:"value2",
key3:"value3",
key4:5,
key5:true,
key6:"val6"
}
},
methods: {
resetModalData: function(){
let stringDefault="";
let numberDefault=0;
let booleanDefault=false;
Object.keys(this.modalData).forEach(key => {
if(typeof(this.modalData[key])==="number"){
this.modalData[key]=numberDefault;
}else if(typeof(this.modalData[key])==="boolean") {
this.modalData[key]=booleanDefault;
}else{
// default type string
this.modalData[key]=stringDefault;
}
});
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
{{modalData}}
<br/>
<button #click="resetModalData">Reset Modal Data</button>
</div>
update : in your case :
data:{
species: this.speciedata,
modalData:{
newCategory: '',
newName: '',
newAmount:''
}
},
and after storing data :
storeSpecie: async function() {
axios.post('/specie/store', {
category: this.newCategory,
name: this.newName,
amount: this.newAmount,
})
.then(()=>{
this.addRow();
this.resetModalData();
this.hideModal('add')
}
},
In native Javascript you get the reset() method.
Here is how it is used :
document.getElementById("myForm").reset();
It will clear every input in the form.

Resources