The dynamically created components in vue.js shows random behaviour - javascript

What i want is, there a list made from json data. When i click on a item, it creates a new list dynamically.
Now when i click a different item in the first list, i want the second list to change depending on data i receive.
html structure is :
div class="subject-list container-list" id="app-1">
<item-subject
v-for="item in subjectlist"
v-bind:item="item"
v-bind:key="item.id"
>
</item-subject>
</div>
//some other code
<div class="exam-list container-list" id="app-2">
<item-exam
v-for="item in examlist"
v-bind:item="item"
v-bind:key="item.id"
>
</item-exam>
</div>
The main.js file is :
//Dummy json data
var subjects_json = { 'subjectlist': [
{ id: 0, text: 'Computer Graphics' },
{ id: 1, text: 'Automata Theory' },
{ id: 2, text: 'Programming in C' }
]};
var exams_json = { 'examlist': [
{ id: 0, text: 'IAT 1' },
{ id: 1, text: 'IAT 2' },
{ id: 2, text: 'Sem 2' }
]};
/*Contains definition of component item-subject...
Its method() contains call to exam component because it will be
called depending on the subject selected dynamically*/
Vue.component('item-subject', {
props: ['item'],
template: '<li v-on:click="showExams" class="subject-list-item">{{
item.text }}</li>',
methods: {
showExams: function(){
// alert(this.item.text)
console.log("Subject Clicked: "+this.item.text)
var app2 = new Vue({
el: '#app-2',
data: exams_json,
methods: {
showStudents: function(){
console.log("exams rendered")
}
}
})
},
}
});
//Contains definition of component item-exam.
Vue.component('item-exam', {
props: ['item'],
template: '<li v-on:click="showStudents" class="exam-list-item">{{ item.text }}</li>',
methods: {
showStudents: function(){
alert(this.item.text)
console.log("exam component executed")
// console.log("Exam Clicked: "+this.item)
}
}
});
//Call to subject component
var app1 = new Vue({
el: '#app-1',
data: subjects_json,
methods: {
showExams: function(){
console.log("subjects rendered")
}
}
})
So what this code does is, when i click on the first list i.e. subjects list, it dynamically renders new exams list.
Now when i click on second list, alert() method is called successfully.
However if i click any of the subject list(first list), now the alert() is not triggered while clicking second list.
Please tell me whats wrong.

Related

Components inside components VueJS

I'm new to VueJs and I'm having trouble with the following (excuse me in advance if the question seems dumb but I haven't found an answer to this yet):
var data = {
info: [
{
brand: 'Samsung',
model: 'A9834',
color: 'black',
id: 0
},
{
brand: 'Nokia',
model: 'Z9234',
color: 'blue',
id: 2
}
]
}
Vue.component('list-group', {
template: '<ul class="list-group"><list-group-item v-for="item in info" v-bind:key="item.id" v-bind:properties="item"></list-group-item></ul>'
})
Vue.component('list-group-item', {
template: '<li class="list-group-item">{{properties.brand}}, {{properties.model}}, {{properties.color}}</li>',
props: ['properties']
})
var instance = new Vue({
el: '.app',
data: data
})
What I'm trying to do with the snippet above is render the component inside the component. The error I get in the console is the following:
[Vue warn]: Property or method "info" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
As you can see, what I expect to get is two li elements inside a ul element (2 elements because I'm looping data.info with a v-for, and data.info has two elements for simplicity); but instead i'm getting the error above. There is obviously something I'm missing here knowledge-wise but I don't know what it is. If you could help me figure it out, I would appreaciate it a lot.
Thanks in advance.
Your code works fine with props: ['info']
https://codepen.io/bsalex/project/editor/ApjgQd#
var data = {
info: [
{
brand: "Samsung",
model: "A9834",
color: "black",
id: 0
},
{
brand: "Nokia",
model: "Z9234",
color: "blue",
id: 2
}
]
};
Vue.component("list-group", {
props: ["info"],
template:
'<ul class="list-group"><list-group-item v-for="item in info" v-bind:key="item.id" v-bind:properties="item"></list-group-item></ul>'
});
Vue.component("list-group-item", {
template:
'<li class="list-group-item">{{properties.brand}}, {{properties.model}}, {{properties.color}}</li>',
props: ["properties"]
});
var instance = new Vue({
el: ".app",
template: '<list-group :info="info">',
data: data
});

Rendering a component directive in Vue 2 without params

I have an app that holds the student list data. The component is supposed to take in that list and render a select dropdown (with select2).
In the fiddles console, it's displaying jQuery is not defined. I thought all fiddles now included jQuery?
I'm really not sure why this is breaking the all together. Is there something wrong with my directive? I know with Vue 2.0 they removed params, but this should suffice. Any eyes on my code would be greatly appreciated.
// Define component
var studentsComponent = Vue.extend({
props: ['students'],
data(): {
return {}
},
methods:{},
directives: {
select: {
bind: function () {
var self = this;
var select = $('#select-student');
select.select2();
select.on('change', function () {
console.log('hey on select works!');
});
},
update: function (oldVal, newVal) {
var select = $('#select-student');
select.val(newVal).trigger('change');
}
},
},
template: `
<div>
<select
ref=""
id="select-student"
v-select>
<option value="0">Select Student</option>
<option
v-for="(student, index) in students"
:value="student.id">
{{ student.name }}
</option>
</select>
</div>
`,
});
// Register component
Vue.component('students-component', studentsComponent);
// App
new Vue({
el: '#app',
data: {
students: [
{ name: 'Jack', id: 0 },
{ name: 'Kate', id: 1 },
{ name: 'Sawyer', id: 2 },
{ name: 'John', id: 3 },
{ name: 'Desmond', id: 4 },
]
},
});
I made a fiddle https://jsfiddle.net/hts8nrjd/4/ for reference. Thank you for helping a noob out!
First, as I mentioned in comments, I would suggest you do this with a component. If you had to stick with a directive, however, you can't initialize select2 in the bind hook. You've defined your options in the DOM, so you need to wait until the component is inserted to initialize it.
directives: {
select: {
inserted: function (el, binding, vnode) {
var select = $(el);
select.select2();
select.on('change', function () {
console.log('hey on select works!');
});
},
},
},
Here is an update of your fiddle.

Can't Target repeated component in DOM - Vue.js

Excuse any syntax errors, It works perfectly, but I could have made an error copying over.
Problem: I have a component, 'dropdown', that is repeated three times with
v-for='(item, index) in search'
which is an array with three objects. Below in the 'filterInput' method, The for loop and if statement does indeed work as intended, HOWEVER, I do not know how to target the 'dropdown' element that matches the search[i]. I need to remove the search[i]'s element in the DOM when the search[i].text doesn't match the input.
<div id='app'>
<input type='text' placeholder='Start typing here...' v-model='input'
#click='drop()' v-on:blur='close()'>
<ul id="dropdown" class='nodisplay'>
<dropdown v-for='(item, index) in search' v-bind:item='item' v-
bind:index='index'></dropdown>
</ul>
</div>
Vue.component('dropdown', {
props: ['item', 'index'],
template: `<li> {{item.text}}</li>`
})
var app = new Vue({
el: '#app',
data: {
input: '', //reactive
search: [
{id: 1, text: 'Jacob'},
{id: 2, text: 'Jeff'},
{id: 3, text: 'Tom'}
]
},
methods: {
drop: function() {
let dropdown = document.getElementById('dropdown');
dropdown.classList.toggle('nodisplay');
},
close: function() {
let dropdown = document.getElementById('dropdown');
dropdown.classList.toggle('nodisplay');
document.querySelector('input').value = '';
},
filterInput: function(index) {
//dropdown items in console: app.search[index].text = 'xyz'
for (let i = 0; i < this.search.length; i++) {
if (!(this.search[i].text.startsWith(this.input))) {
//hide or remove this current search element from dropdown
}
}
}
},
watch: {
input: function() {
this.filterInput();
}
}
})
tl;dr; how do I target
What you are looking for is how to have parent child communication, which I have answered today itself here.
You need to $emit an event from the child component and set the value used in input field, just like the example in documentation.
Here is the code:
HTML
<div id='app'>
<input type='text' placeholder='Start typing here...' v-model='input'
#click='drop()' >
<ul id="dropdown" :class="{'nodisplay': dropDownClosed}">
<dropdown v-for='(item, index) in search' v-bind:item='item' v-
bind:index='index' v-on:getdropdowninput="getdropdowninput"></dropdown>
</ul>
</div>
JS
dropdown = Vue.component('dropdown', {
props: ['item', 'index'],
template: `<div><li ><a #click="selectval(item.text)" href="#"> {{item.text}}</a></li></div>`,
methods: {
selectval (value) {
this.$emit("getdropdowninput", value)
}
}
})
var app = new Vue({
el: '#app',
data: {
input: '', //reactive
dropDownClosed: false,
search: [
{id: 1, text: 'Jacob'},
{id: 2, text: 'Jeff'},
{id: 3, text: 'Tom'}
]
},
methods: {
drop: function() {
this.dropDownClosed = true
},
getdropdowninput: function(value) {
this.dropDownClosed = false
this.input = value;
},
filterInput: function(index) {
//dropdown items in console: app.search[index].text = 'xyz'
for (let i = 0; i < this.search.length; i++) {
if (!(this.search[i].text.startsWith(this.input))) {
//hide or remove this current search element from dropdown
}
}
}
},
watch: {
input: function() {
this.filterInput();
}
}
})
Here is the working fiddle.
Use dynamic classes: I have also modified how to add/remove a class dynamically in vue way, instead of document.getElementById. Notice in following line:
<ul id="dropdown" :class="{'nodisplay': dropDownClosed}">
nodisplay class will be applied when dropDownClosed variable will be true and it will be removed when dropDownClosed variable will be false.
How to Filter:
To filter, you can use a computed property in the v-for and whenever input changes you can filter the search array, like following
computed: {
filteredInput: function(){
if(this.input === '' || !this.input){
return this.search
} else {
var self = this
return this.search.filter(
function( s ) {
return s.text.indexOf( self.input ) !== -1; }
);
}
}
See working fiddle here.

Unable to create a delete button in Meteor using reactive-table

I building a sortable table in Meteor with Reactive-Table and having trouble getting my delete button to work for removing entries from the table.
Please see my javascript code below:
Movies = new Meteor.Collection("movies");
if (Meteor.isClient) {
Template.body.events({
"submit .new-movie": function (event) {
var title = event.target.title.value;
var year = event.target.year.value;
var genre = event.target.genre.value;
Movies.insert({
title: title,
year: year,
genre: genre
});
event.target.title.value = "";
event.target.year.value = "";
event.target.genre.value = "0";
return false;
}
});
Template.moviestable.events({
"click .deletebtn": function (event) {
Movies.remove(this._id);
}
});
Template.moviestable.helpers({
movies : function () {
return Movies.find();
},
tableSettings : function () {
return {
showFilter: false,
fields: [
{ key: 'title', label: 'Movie Title' },
{ key: 'year', label: 'Release Year' },
{ key: 'genre', label: 'Genre' },
{ key: 'edit', label: 'Edit', fn: function () { return new Spacebars.SafeString('<button type="button" class="editbtn">Edit</button>') } },
{ key: 'delete', label: 'Delete', fn: function () { return new Spacebars.SafeString('<button type="button" class="deletebtn">Delete</button>') } }
]
}
}
});
}
Can anyone tell me what I'm doing wrong?
In the reactive tables docs, there's an example of how to delete rows from the table. Adapting the example in the docs for your needs, your event should look like this:
Template.moviestable.events({
'click .reactive-table tbody tr': function (event) {
event.preventDefault();
var objToDelete = this;
// checks if the actual clicked element has the class `deletebtn `
if (event.target.className == "deletebtn") {
Movies.remove(objToDelete._id)
}
}
});
The problem you are having is that you are trying to find the _id property on the button click instead of the row click.
If you do console.log(this) on your button click event (as you have it in your question above) you will get something like this Object {key: "delete", label: "", fieldId: "2", sortOrder: ReactiveVar, sortDirection: ReactiveVar} which does not contain the property _id
It is easier to register the row click, where the row object is the actual document you are trying to delete, and then check if the event's target has the delete class you added.

Meteor Reactive Nested Object Update Parent Object

I'm trying to make a javascript object reactive such that when a sub-object is updated, the original (parent) object is updated - both in the views and in the javascript.
This almost does it except when the "Done?" button is clicked, the console.log output is only accurate the FIRST time. After that, toggling any sub-items correctly updates the view but the underlying javascript is NOT updated as the "selected" values for the subitems are no longer correct.
Moreover, I'd like to be able to simplify and simply have the parent items object update when any sub-items are toggled as selected or not.
I come from AngularJS and this is far simpler - if you pass in an object and then update a property of that object, the parent property is updated as well and kept in sync automatically.
It seems the this and data context are getters but not setters? Template.currentData() seems related but I haven't been able to get that to work:
https://github.com/meteor/meteor/issues/2010
The way I got the items to update was to manually update it by setting and comparing an index to update the main parent object from the inner sub data context but again that's not ideal and gets messier and messier as you get more nested as I'd need ALL nested indices to be able to back-track up to the main items object to then overwrite the whole thing to force an update.
How to Get Parent Data Context in Meteor on an Event
Long story short, I want the given data context (from an event) to be able to be a setter such that if I'm in an {{#each }} and happen to be on items[1].subitem[2] then I can just do this.selected =!this.selected and that will then effectively update items[1].subitem[2].selected (reactively - updating both the javascript object itself and the DOM). So then later I can check the value of items[1].subitem[2].selected and know it's reliable and accurate. Currently I'm getting the DOM to update based on the selected boolean but the parent items array is inaccurate.
HTML / template:
<template name="items">
{{#each items}}
<div class='item-title'>{{title}}</div>
<div class='{{classes.cont}}'>
{{#each subitem}}
{{> subitems }}
{{/each}}
</div>
{{/each}}
<div class='btn btn-primary btn-block items-done'>Done?</div>
</template>
<template name="subitems">
<div class='flexbox subitems'>
<div class='flex1 pointer'>{{title}}</div>
{{#if selected}}
<div class='fa fa-check-circle'></div>
{{else}}
<div class='fa fa-plus-circle'></div>
{{/if}}
</div>
</template>
Javascript
if (Meteor.isClient) {
var items1 =[
{
title: 'Item 1',
subitem: [
{
title: 'Sub 1.1'
},
{
title: 'Sub 1.2'
},
{
title: 'Sub 1.3'
}
]
},
{
title: 'Item 2',
subitem: [
{
title: 'Sub 2.1'
},
{
title: 'Sub 2.2'
},
{
title: 'Sub 2.3'
}
]
}
];
var ii, jj;
for(ii =0; ii<items1.length; ii++) {
items1[ii].classes ={
cont: 'hidden'
};
items1[ii].index =ii; //Blaze / Spacebars does not yet give access to index? Maybe #index in html but no equivalent in `this` in javascript?
for(jj =0; jj<items1[ii].subitem.length; jj++) {
items1[ii].subitem[jj].selected =false;
items1[ii].subitem[jj].index =jj;
}
}
Template.items.created =function() {
this.items =new ReactiveVar;
this.items.set(items1);
};
Template.items.helpers({
items: function() {
return Template.instance().items.get();
}
});
Template.items.events({
'click .item-title': function(evt, template) {
var items =template.items.get();
if(this.classes.cont ==='visible') {
this.classes.cont ='hidden';
}
else {
this.classes.cont ='visible';
//hide all others
for(ii =0; ii<items.length; ii++) {
if(ii !==this.index) {
items[ii].classes.cont ='hidden';
}
}
}
template.items.set(items);
},
'click .items-done': function(evt, template) {
console.log(template.items.get()); //TESTING
}
});
Template.subitems.created =function() {
this.selected =new ReactiveVar;
this.selected.set(this.data.selected);
};
Template.subitems.helpers({
selected: function() {
return Template.instance().selected.get();
}
});
Template.subitems.events({
'click .subitems': function(evt, template) {
//toggle selected
this.selected =!this.selected;
template.selected.set(this.selected);
}
});
}
Switching to a (local only) collection seemed to make it reactive and I had to indeed update the root (parent) item (using Template.parentData()).
So basically converting to and exclusively using as a collection works. A bit annoying and seems a bit like a hack but I suppose that's the point of meteor and blurring the lines between server and client - just use collections for everything and let the built in meteor reactivity work its magic.
https://www.eventedmind.com/feed/meteor-ui-reactivity-in-slow-motion
if (Meteor.isClient) {
var items1 =[
{
title: 'Item 1',
subitem: [
{
title: 'Sub 1.1'
},
{
title: 'Sub 1.2'
},
{
title: 'Sub 1.3'
}
]
},
{
title: 'Item 2',
subitem: [
{
title: 'Sub 2.1'
},
{
title: 'Sub 2.2'
},
{
title: 'Sub 2.3'
}
]
}
];
var ii, jj;
for(ii =0; ii<items1.length; ii++) {
items1[ii].classes ={
cont: 'hidden'
};
items1[ii].index =ii; //Blaze / Spacebars does not yet give access to index? Maybe #index in html but no equivalent in `this` in javascript?
for(jj =0; jj<items1[ii].subitem.length; jj++) {
items1[ii].subitem[jj].selected =false;
items1[ii].subitem[jj].index =jj;
}
}
ItemCollection = new Meteor.Collection(null);
for(ii =0; ii<items1.length; ii++) {
ItemCollection.insert(items1[ii]);
}
Template.items.helpers({
items: function() {
return ItemCollection.find();
}
});
Template.items.events({
'click .item-title': function(evt, template) {
var items =ItemCollection.find().fetch();
if(this.classes.cont ==='visible') {
this.classes.cont ='hidden';
}
else {
this.classes.cont ='visible';
//hide all others
for(ii =0; ii<items.length; ii++) {
if(ii !==this.index) {
//items[ii].classes.cont ='hidden';
ItemCollection.update(items[ii]._id, {$set: {classes: {cont: 'hidden'} } });
}
}
}
//update current item
ItemCollection.update(this._id, {$set: {classes: {cont: this.classes.cont } } });
},
'click .items-done': function(evt, template) {
console.log(ItemCollection.find().fetch()); //TESTING
}
});
Template.subitems.events({
'click .subitems': function(evt, template) {
//toggle selected
this.selected =!this.selected;
var itemId =Template.parentData(1)._id;
var setObj ={};
setObj['subitem.'+this.index+'.selected'] =this.selected;
console.log(itemId+' '+JSON.stringify(setObj)); //TESTING
ItemCollection.update(itemId, { $set: setObj});
}
});
}

Categories

Resources