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

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.

Related

TipTap Vue component - how to toggle wrap on node from component button

The awesome tiptap wrapper for prosemirror comes with nice documentation but it lacks some clarification how to approach some (i think) basic scenarios when developing custom extensions.
My question is how to invoke toggleWrap on the node when in vue component's context.
I found example that uses transactions and allows for delete - but what i want is to clear the node leaving the text of node intact.
get view() {
return {
directives: {
"click-outside": clickOutside
},
props: ['node', 'updateAttrs', 'view', 'selected', 'getPos'],
data() {
return {
showMenu: false
}
},
computed: {
href: {
get() {
return this.node.attrs.href
},
set(href) {
this.updateAttrs({
href,
})
},
},
},
methods: {
// deleteNode() {
// let transaction = this.view.state.tr // tr - transaction
// let pos = this.getPos()
// transaction.delete(pos, pos + this.node.nodeSize)
// this.view.dispatch(transaction)
// },
stopLinkPropagation(){
return null;
},
hideMenu(){
this.showMenu = false
}
},
template: `<div #click="showMenu = true" v-click-outside="hideMenu">
<a class="email-button" #click.prevent="stopLinkPropagation" :href="href" v-text="node.textContent"></a>
<input class="iframe__input" type="text" v-model="href" v-if="showMenu" />
<button #click="clearNode">clear button wrap</button>
</div>`,
}
}
Any help would be awesome. Thanks.

V-model is not listening to value change for an input (vuejs)

I have an object property which could listen to the user input or could be changed by the view.
With the snipped below :
if I typed something the value of my input is updated and widget.Title.Name is updated.
if I click on the button "External Update", the property widget.Title.Name is updated but not the value in my field above.
Expected result : value of editable text need to be updated at the same time when widget.Title.Name change.
I don't understand why there are not updated, if I inspect my property in vue inspector, all my fields (widget.Title.Name and Value) are correctly updated, but the html is not updated.
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function () {
this.widget.Title.Name = "changed title";
},
}
})
button{
height:50px;
width:100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable"
v-on="listeners">
</p>
</template>
I searched a lot of subject about similar issues but they had reactivity problem, I think I have a specific problem with input. Have you any idea of what's going on ? I tried to add a listener to change event but it was not triggered on widget.Title.Name change.
To anwser to this problem, you need to do 3 differents things.
Add watch property with the same name as your prop (here value)
Add debounce function from Lodash to limit the number of request
Add a function to get back the cursor (caret position) at the good position when the user is typing
For the third point : when you change the value of widget.Title.Name, the component will re-render, and the caret position will be reinitialize to 0, at the beginning of your input. So, you need to re-update it at the last position or you will just write from right to left.
I have updated the snippet above with my final solution.
I hope this will help other people coming here.
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
//Added watch value to watch external change <-> enter here by user input or when component or vue change the watched property
watch: {
value: function (newVal, oldVal) { // watch it
// _.debounce is a function provided by lodash to limit how
// often a particularly expensive operation can be run.
// In this case, we want to limit how often we update the dom
// we are waiting for the user finishing typing his text
const debouncedFunction = _.debounce(() => {
this.UpdateDOMValue();
}, 1000); //here your declare your function
debouncedFunction(); //here you call it
//not you can also add a third argument to your debounced function to wait for user to finish typing, but I don't really now how it works and I didn't used it.
}
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
},
UpdateDOMValue: function () {
// Get caret position
if (window.getSelection().rangeCount == 0) {
//this changed is made by our request and not by the user, we
//don't have to move the cursor
this.$refs["editable-text"].innerText = this.value;
} else {
let selection = window.getSelection();
let index = selection.getRangeAt(0).startOffset;
//with this line all the input will be remplaced, so the cursor of the input will go to the
//beginning... and you will write right to left....
this.$refs["editable-text"].innerText = this.value;
//so we need this line to get back the cursor at the least position
setCaretPosition(this.$refs["editable-text"], index);
}
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function () {
this.widget.Title.Name = "changed title";
},
}
})
/**
* Set caret position in a div (cursor position)
* Tested in contenteditable div
* ##param el : js selector to your element
* ##param caretPos : index : exemple 5
*/
function setCaretPosition(el, caretPos) {
var range = document.createRange();
var sel = window.getSelection();
if (caretPos > el.childNodes[0].length) {
range.setStart(el.childNodes[0], el.childNodes[0].length);
}
else
{
range.setStart(el.childNodes[0], caretPos);
}
range.collapse(true);
sel.removeAllRanges();
}
button{
height:50px;
width:100px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable"
v-on="listeners">
</p>
</template>
you can use $root.$children[0]
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
computed: {
listeners() {
return {...this.$listeners, input: this.onInput
};
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function(e) {
this.widget.Title.Name = "changed title";
this.$root.$children[0].$refs["editable-text"].innerText = "changed title";
},
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners">
</p>
</template>
or use Passing props to root instances
Vue.component('editable-text', {
template: '#editable-text-template',
props: {
value: {
type: String,
default: '',
},
contenteditable: {
type: Boolean,
default: true,
},
},
computed: {
listeners() {
return {...this.$listeners, input: this.onInput
};
},
},
mounted() {
this.$refs["editable-text"].innerText = this.value;
this.$root.$on("titleUpdated",(e)=>{
this.$refs["editable-text"].innerText = e;
})
},
methods: {
onInput(e) {
this.$emit('input', e.target.innerText);
}
}
});
var vm = new Vue({
el: '#app',
data: {
widget: {
Title: {
Name: ''
}
}
},
async created() {
this.widget.Title.Name = "toto"
},
methods: {
externalChange: function(e) {
this.widget.Title.Name = "changed title";
this.$root.$emit("titleUpdated", this.widget.Title.Name);
},
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="app">
<editable-text v-model="widget.Title.Name"></editable-text>
<template>Name : {{widget.Title.Name}}</template>
<br>
<br>
<button v-on:click="externalChange">External update</button>
</div>
<template id="editable-text-template">
<p ref="editable-text" v-bind:contenteditable="contenteditable" v-on="listeners">
</p>
</template>

Vue Test Utils + Third-party component: cannot detect input event

I'm passing through a problem to detect the input event emission while using a third-party component only when testing. The component behaves as it's supposed to when testing on the browser. Using the Vue extension I also can see that it's emitting the event correctly.
I'm using Choices as my custom select.
Vue.component('choices', {
props: ['options', 'value'],
template: '#choices-template',
mounted: function() {
this.choicesInstance = new Choices(this.$refs.select);
this.$refs.select.addEventListener('addItem', this.handleSelectChange);
this.setChoices();
},
methods: {
handleSelectChange(e) {
this.$emit('input', e.target.value);
},
setChoices() {
this.choicesInstance.setChoices(this.options, 'id', 'text', true);
}
},
watch: {
value: function(value) {
console.log('input triggered: ' + value);
},
options: function(options) {
// update options
this.setChoices();
}
},
destroyed: function() {
this.choicesInstance.destroy();
}
})
var vm = new Vue({
el: '#el',
template: '#demo-template',
data: {
selected: 2,
options: [{
id: 1,
text: 'Hello'
},
{
id: 2,
text: 'World'
}
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css" />
<div id="el"></div>
<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template" id="demo-template">
<div>
<p>Selected: {{ selected }}</p>
<choices :options="options" v-model="selected">
<option disabled value="0">Select one</option>
</choices>
</div>
</script>
<script type="text/x-template" id="choices-template">
<select ref="select">
<slot></slot>
</select>
</script>
My test file is like this (I'm using Jest):
import CustomSelect from '#/CustomSelect';
import { mount } from '#vue/test-utils';
let wrapper,
options = [
{
key: 'Foo',
value: 'foo',
},
{
key: 'Bar',
value: 'bar',
},
{
key: 'Baz',
value: 'baz',
},
];
beforeEach(() => {
wrapper = mount(CustomSelect, {
propsData: {
options,
},
});
});
it('Emits an `input` event when selection changes', () => {
wrapper.vm.choicesInstance.setChoiceByValue([options[1].value]);
expect(wrapper.emitted().input).not.toBeFalsy();
});
wrapper.emitted() is always an empty object for this test;
If I expose choicesInstance from my component to global context and manually call choicesInstance.setChoiceByValue('1') it correctly emits the input event.

The dynamically created components in vue.js shows random behaviour

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.

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.

Categories

Resources