Vue - Dynamic component event listener - javascript

Problem : I am trying to create a table component for my app which would be use by other components to render a table. It could have three possible cell values :
Text
HTML
Component
I am able to render all the above values but I am stuck at binding an event listener. What I am trying to achieve is something like this :
Pass a method and event which is to be binded to the component and the table should bind that with respective cell.
So for example :
TABLE JSON
{
"cell-1":{
"type":"html",
"data":"<h4>text-1</h4>",
"method": someMethod
}
}
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit(someMethod)"
v-on:click.native="someMethod"
></span>
</td>
</tr>
</template>
</tbody>
Above is just a snippet of what I am trying, the table loops through the object passed and renders accordingly.
I have already tried
SO Solution 1
SO Solution 2
Please let me know if any more info is required.

The best way is to have the method/handler inside the parent component and then trigger is using the emit functionality such that in
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit('trigger-handler', {method: 'method1', data: {}})"
></span>
</td>
</tr>
</template>
</tbody>
and in
Parent.vue
<table-component #trigger-handler="triggerHandler" />
inside script
export default {
data() {
},
methods: {
triggerHandler(payload) {
// payload is actually the object passed from the child
this[payload.method](payload.data); // call a specific method
},
method1(data) {
},
method2(data) {
},
method3(data) {
}
}
}

Related

VueJS Render VNode

tl;dr:
Given a VueJS VNode object, how do I get the HTML element that would be generated if it were rendered?
e.g.:
> temp1
VNode {tag: "h1", data: undefined, children: Array(1), text: undefined, elm: undefined, …}
> temp1.children[0]
VNode {tag: undefined, data: undefined, children: undefined, text: "Test", elm: undefined, …}
> doSomething(temp1)
<h1>Test</h1>
Goal
I'm attempting to build a small VueJS wrapper around the DataTables.net library.
To mimic the behavior of HTML tables in my markup, I want something like the following:
<datatable>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<datatable-row v-for="person in people">
<td>{{ person.name }}</td>
<td>{{ person.age }}</td>
<td>{{ person.salary }}</td>
</datatable-row>
</tbody>
</datatable>
What I've done so far
I've started to implement this as follows:
DataTable.vue
<template>
<table ref="table" class="display table table-striped" cellspacing="0" width="100%">
<slot></slot>
</table>
</template>
<script>
/* global $ */
export default {
data: () => ({
instance: null
}),
mounted() {
this.instance = $(this.$refs.table).dataTable();
this.$el.addEventListener("dt.row_added", function(e) {
this.addRow(e.detail);
});
},
methods: {
addRow(row) {
// TODO <-----
console.log(row);
}
}
};
</script>
DataTableRow.vue
<script>
/* global CustomEvent */
export default {
mounted() {
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent("dt.row_added", {
bubbles: true,
detail: this.$slots.default.filter(col => col.tag === "td")
}));
});
},
render() { return ""; }
};
What this currently does:
When the page loads, the DataTable is initialized. So the column headers are properly formatted and I see "Showing 0 to 0 of 0 entries" in the bottom left
The CustomEvent is able to bubble up past the <tbody> and be caught by the DataTable element successfully (circumventing the limitation in VueJS that you can't listen to events on slots)
What this does not do:
Actually add the row
My event is giving me an array of VNode objects. There's one VNode per column in my row. The DataTables API has an addRow function which can be called like so:
this.instance.row.add(["col1", "col2", "col3"]);
In my case, I want the resultant element from the rendering of the VNode to be the elements in this array.
var elems = [];
for (var i = 0; i < row.length; i++)
elems[i] = compile(row[i]);
this.instance.row.add(elems);
Unfortunately this compile method eludes me. I tried skimming the VueJS documentation and I tried Googling it, but no dice. I tried manually passing the createElement function (the parameter passed to the render method) but this threw an error. How can I ask VueJS to render a VNode without injecting the result into the DOM?
I ran into the same issue wanting to do basically the same thing with a row details template for DataTables.net.
One solution could be to create a generic component that renders out a VNode and instantiate that programmatically. Here is how my setup for a dynamic detail row that I insert using datatable's row.child() API.
RenderNode.js
export default {
props: ['node'],
render(h, context) {
return this.node ? this.node : ''
}
}
Datatables.vue
Include the renderer component from above
import Vue from 'vue'
import nodeRenderer from './RenderNode'
Instantiate and mount the renderer to get the compiled HTML
// Assume we have `myVNode` and want its compiled HTML
const DetailConstructor = Vue.extend(nodeRenderer)
const detailRenderer = new DetailConstructor({
propsData: {
node: myVNode
}
})
detailRenderer.$mount()
// detailRenderer.$el is now a compiled DOM element
row.child(detailRenderer.$el).show()
You should define your components like with:
import {createApp} from 'vue';
import {defineAsyncComponent} from "vue";
createApp({
components: {
'top-bar': defineAsyncComponent(() => import('#Partials/top-bar'))
}
}).mount("#app")

Vue2 modal in v-for list

I'm trying to implement a vue2 modal as described in the vue docs at https://v2.vuejs.org/v2/examples/modal.html.
It looks something like this:
<tbody v-if="fields.length">
<tr v-for="(item, index) in fields">
<td>#{{ item.name }}</td>
<td><input type="checkbox" id="checkbox" v-model="item.active"></td>
<td>
<button id="show-modal" #click="showModal = true">Show Modal</button>
</td>
</tr>
<modal :item="item" v-if="showModal" #close="showModal = false">
<h3 slot="header">Hello World</h3>
</modal>
</tbody>
Vue.component('modal', {
template: '#modal-template',
data: function() {
return {
item: ''
}
}
});
While the button shows up and does pop up the modal, I get a warning that Property or method "item" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option., and when I inspect the modal component in the Vue dev tools, the item is still '', it's not populated by the :item binding.
What's the right way to pass the item object into the modal so we can use it in the window?
If you're passing item as a value from the parent to a child component, you cannot use data--you must use props instead!
Vue.component('modal', {
template: '#modal-template',
props: ['item'],
data: function() {
return {
}
}
});
There are 2 things:
You don't reference item in the data object instead you should reference it as a prop for the component props: ['item'].
The modal is not inside the loop's scope, which ends at the closing </tr> tag. You could change this so that the click action performs a method that takes the item and assigns it to a variable that is always passed in like a prop. This would be similar to how you are doing it now, you would just change showModal = true to openModal(item) then have a method that would set the appropriate 2 values. Something like this:
openModal(item) {
this.showModal = true;
this.modalItem = item;
}
...
data: () => {
modalItem: null
...
}

How to use new Vue in a single file component?

I'm still learning Vue.js and I have a minor issue:
I have a single file component with an array of checkboxes, and I have looked at the documentation for using multiple checkboxes, but the example there requires me to declare:
new Vue({
el: '#example-3',
data: {
checkedNames: []
}
})
Edit However I have set this up in my single file component within the <script> tag within data(), but it just checks/unchecks all boxes at once (but feeds back true/false correctly):
<script>
export default {
name: 'PhrasesDetail',
data () {
return {
game: '',
language: '',
checkedPhrasesArr: {
el: '#selection',
data: {
checkedPhrasesArr: []
}
}
}
},
...
</script>
The Question is where and how do I declare the checkbox array so that it reacts/recognises the individual elements?
These are my checkboxes:
<tr v-for="(keyTransPair, index) in this.language.data">
<td id="selection"><input type="checkbox" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
...
</tr>
I have assembled a complete example. (I don´t know anything about your language.data object so I´m just using fake data).
<template>
<div>
<!-- Your Checkboxes -->
<table>
<tr v-for="(keyTransPair, index) in language.data">
<td id="selection"><input type="checkbox" :value="keyTransPair" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
</tr>
</table>
<!-- Show the selected boxes -->
{{ checkedPhrasesArr }}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
// Your language data
language: {
data: {
keyOne: "one",
keyTwo: "two",
keyThree: "three"
}
},
// The Checkbox data
checkedPhrasesArr: []
}
}
}
</script>
Note that the checkbox values are bound with :value="keyTransPair". You can change the value to anything you want. This value will be added to the array if the checkbox is checked.
By the way: You use <td id="selection"> within the v-for loop. So the id "selection" will not be unique. You should better use a class instead of an id here.

vue.js How to call a function inside a for loop and pass the current item?

Using vue.js 2, inside a for loop, I need to render a row only if the current iterated item passes some test.
The test is complex so a simple v-if="item.value == x" wont do.
I've written a function named testIssue that accepts the item and returns true or false and tried to use that is av-if like this:
<template v-for="issue in search_results.issues">
<tr v-if="testIssue(issue)">
....
</tr>
</template>
var releaseApp = new Vue({
el: '#release-app',
methods: {
testIssue: function(issue) {
console.log(issue);
console.log('test');
},
},
mounted: function() {},
data: {
search_results: {
issues: []
},
}
});
However, testIssue is never called.
If I change the line to <tr v-if="testIssue">, the function is called but then I dont have the issue variable that I need to test.
I also tried <tr v-if="releaseApp.testIssue(issue)">
How can I call a function in a v-if declaration inside a for loop and pass the current item?
First of all you can't do v-for on a <template> tag as you can have only one element per template.
You can add a v-if on the same element as v-for, and it just won't render the element that doesn't pass the v-if. If it's a spearate component per row then it's better to do v-for in parent component and pass the issue by props to child component.
Parent:
<child-comp v-for="issue in search_results.issues" v-if="testIssue(issue)">
</child-comp>
Child:
<template>
<tr>
{{issue}}
</tr>
</template>
Your scenario works with a similar example in this fiddle.
But you can try it this way also:
You can create a custom directive named v-hide and pass issue to it as its value.
Then in the directive you can test for testIssue() and set the particular element's display to none
<template v-for="issue in search_results.issues">
<tr v-hide="issue">
....
</tr>
</template>
var releaseApp = new Vue({
el: '#release-app',
directive:{
hide: {
bind(el, binding, Vnode){
var vm = Vnode.context;
var issue = binding.value;
if(vm.testIssue(issue)){
el.style.display = 'none';
}
}
}
},
methods: {
testIssue: function(issue) {
console.log(issue);
console.log('test');
},
},
mounted: function() {},
data: {
search_results: {
issues: []
},
}
});
You can also try creating a computed item that makes use of the filter method so that you have an array where all elements pass the test function before actually rendering (in this case, just returning odd numbers):
https://codepen.io/aprouja1/pen/BZxejL
computed:{
compIssues(){
return this.search_results.issues.filter(el => el%2===1)
}
},

Toggle row independently of others in an {{#each}} - Ember

I've got this chunk of code in my controller to output everything I'd like to in the first row. The second row is going to be available upon a click action. However, I'm trying to make it so that when I click any row shown, it'll only display the 2nd row that given row. Not the 2nd row of all the others.
This is in my template.
{{#each chosenSomething as |something|}}
<tr class='main' {{action 'toggleHelp'}}>
<td>{{something.this}}</td>
<td>{{something.that}}</td>
<td>{{something.hey}}</td>
<td>{{something.you}}</td>
</tr>
{{#if isDisplay}}
<tr class='help'>
<td class='instruction'></td>
<td class='video'></td>
</tr>
{{/if}}
{{/each}}
This is my JS for the action.
toggleHelp() {
this.toggleProperty('isDisplay');
},
I know how I would approach this using jQuery, but I'd like to know the Ember way of doing such action. I thought about including index, but unsure how it should be written.
I would suggest making a component and handing the toggle within.
https://ember-twiddle.com/e6ccad3d2ffdc4a9d627db12a1317fa5?openFiles=templates.components.toggle-thing.hbs%2Ctemplates.components.toggle-thing.hbs
// toggle-thing.hbs
<header {{action 'toggleAnswer'}}>
{{data.question}}
</header>
{{#if open}}
<footer>
{{data.answer}}
</footer>
{{/if}}
// toggle-thing.js
import Ember from 'ember';
export default Ember.Component.extend({
// tagName: 'tr', // or whatever if you like
// classNames: ['thing', 'row', 'etc'], // smooth out the template a bit later...
open: false, // default
actions: {
toggleAnswer() {
this.toggleProperty('open');
console.log( this.get('open') );
},
},
});
// application.hbs or wherever you want to list the things
<ul class='thing-list'>
{{#each model as |thing|}}
<li class='thing'>
{{toggle-thing data=thing}}
</li>
{{/each}}
</ul>
has a 'thing' model and is pulling in some question answers to the model hook for example.
import Ember from 'ember';
var data = [
{
question: "How to be funny",
answer: "Stand on your head",
},
{
question: "How to be smart",
answer: "Just listen more often.",
},
];
export default Ember.Route.extend({
model() {
return data;
},
});
The Ember guide has a toggle example: https://guides.emberjs.com/v2.13.0/tutorial/simple-component/
Augment each element of chosenSomething to include the open/close state.
You'll then be able to have:
{{#each chosenSomething as |something|}}
<tr class='main' {{action 'toggleHelp' something}}>
<td>{{something.this}}</td>
<td>{{something.that}}</td>
<td>{{something.hey}}</td>
<td>{{something.you}}</td>
</tr>
{{#if something.isDisplay}}
<tr class='help'>
<td class='instruction'></td>
<td class='video'></td>
</tr>
{{/if}}
{{/each}}
and the JS:
toggleHelp(something) {
Ember.set(something, 'isDisplay', !something.isDisplay);
}
If you don't want to modify the original data, you can have a computed property generate the appropriate data such as:
chosenSomethingWithState() {
return (this.get('chosenSomething') || []).map((item)=>{isDisplay:false, item})
}.property('chosenSomething')
and adjust the template accordingly

Categories