How to create router-link programmatically on html render using Datatables.net + Vue.js? - vue.js

I have a Datatables.net jquery plugin as a vue component:
DatatablesCGU:
<template>
<table v-bind="$props" ref="tableElement" class="table table-striped table-hover table-bordered">
<slot></slot>
</table>
</template>
<script>
import $ from 'jquery';
// Datatables
require('datatables.net-bs');
require('datatables.net-bs4/js/dataTables.bootstrap4.js');
require('datatables.net-buttons');
require('datatables.net-buttons-bs');
require('datatables.net-responsive');
require('datatables.net-responsive-bs');
require('datatables.net-responsive-bs/css/responsive.bootstrap.css');
require('datatables.net-buttons/js/buttons.colVis.js'); // Column visibility
require('datatables.net-buttons/js/buttons.html5.js'); // HTML 5 file export
require('datatables.net-buttons/js/buttons.flash.js'); // Flash file export
require('datatables.net-buttons/js/buttons.print.js'); // Print view button
require('datatables.net-keytable');
require('datatables.net-keytable-bs/css/keyTable.bootstrap.css');
require('datatables.net-select');
require('jszip/dist/jszip.js');
require('pdfmake/build/pdfmake.js');
require('pdfmake/build/vfs_fonts.js');
//Evita o alert chato do datatables em caso de erro
$.fn.dataTable.ext.errMode = function ( settings, helpPage, message ) {
console.error(message);
};
/**
* Wrapper component for dataTable plugin
* Only DOM child elements, componets are not supported (e.g. <Table>)
*/
export default {
name: 'DatatableCGU',
props: {
/** datatables options object */
options: { type: Function, "default": ()=>{} },
/** callback that receives the datatable instance as param */
dtInstance: Function
},
data(){
return { datatables : null}
},
mounted() {
const dtInstance = $(this.$refs.tableElement).DataTable(this.options());
this.datatables = dtInstance;
if (this.dtInstance) {
this.dtInstance(dtInstance);
}
this.$root.$on('filtrar', this.refresh);
},
destroyed() {
$(this.$refs.tableElement).DataTable({destroy: true});
},
methods: {
refresh(filtros) {
this.datatables.ajax.reload();
}
}
}
</script>
On another component, i use this passing a datatables options with some custom renders on columns properties:
...
methods: {
getOptions(){
let options = this.getDefaultOptions();
options.ajax.url = "/api/auth/usuarios";
options.filtrador = this.filtrador;
options.columns = [
this.colunaDeSelecao(this.modoPopup)
,{name: "cpf", data: "cpf", title: "CPF"}
,{name: "nome", data: "nome", title: "Nome"}
,{name: "email", data: "email", title: "E-mail"}
,{name: "id", data: "id", title: "Ações", visible: !(this.modoPopup), sortable:false, className:"dt-center", width: "200px", render: function(data, type, row) {
return `<span class='btn-group btn-group-sm'>
<button id='btnAlternar__${data}' data-id='${data}' class='btn btn-${row.ativo?"danger":"success"}' data-toggle='tooltip' title='${row.ativo?"Inativar":"Ativar"}'><i class='fas fa-power-off'></i></button>
<a href='${window.$baseURL}auth/usuarios/${data}' class='btn btn-warning' data-toggle='tooltip' title='Editar'><i class='far fa-edit'></i></a>
</span>`;
}}
];
options.initComplete = () =>{
this.getDefaultOptions().initComplete();
this.criarTogglersSituacao();
};
return options;
}
...
If you notice the last column render creates a <a href='${window.$baseURL}auth/usuarios/${data}' ... that obviously isn't a router-link and doesn't trigger vue router properly, causing an undesired page refresh.
I need the link to do a router push instead of a page refresh. How is this possible?

There is no good answer to that problem. datatables is not really compatible with Vue.js. With Vue, the usual way to go would be to pass your reactive HTML structure within a slot to such a library. Because datatables requires you to use a render function and return static HTML as a string, you cannot pass any JavaScript logic along.
The main problem is that you need to pass an event from the link tag to the Vue component. One approach would be to pass HTML in the render function which then can be selected with a specific selector (e.g. adding a class). You also need to add the link data/the item's ID to the HTML element (e.g. with a data-link="" attribute). When datatables has finished rendering, you can add a click listener to all the links. This click listener handler function needs to read the link/ID of the link and pass it to the router. Then, you can use Vue's router.push() function.
When you are implementing a solution with the above approach, make sure to assign and remove the click listeners depending on the lifecycle events of datatables. It might be necessary to add and remove the listeners on each page switch.

Related

Vue.js passing a variable through a modal

I am trying to pass a variable from a Parent (page) component to a Child (modal) component. After reading a few examples, this works fine. The variable in question is brought in from another component as a route param. If i refresh the page, the variable is lost and can no longer be passed to the child. My question is, is the best way to persist this using the store, or is it possible to persist another way if the user refreshed? Any help would be appreciated
Parent
<b-container>
<Modal v-show="displayModal" #close="closeModal">
<template v-slot:body>
<ExpressionCreate v-show="displayModal" :lender-id="lenderId" #close="closeModal"/>
</template>
</Modal>
<div class="card" style="width: 100%;">
<div class="card-header">
<h5>{{this.lenderName}}</h5>
<b-alert :show="this.loading" variant="info">Loading...</b-alert>
</div>
<b-btn block variant="success" #click="showCreateLenderModal">Add Expression</b-btn>
....
created () {
this.lenderId = this.$route.params.lenderId
...
navigate () {
router.go(-1)
},
showCreateLenderModal () {
this.displayModal = true
},
toggleDisplayModal (isShow) {
this.displayModal = isShow
},
async closeModal () {
this.displayModal = false
}
Child
<label>lender id:</label>{{this.lenderId}}
...
props: {
lenderId: {
type: Number
}
},
You can use VueSession to persist.
Simply persist your lender_id with
this.$session.set('lender_id', this.lender_id)
and you can get it later as
saved_lender = this.$session.get('lender_id')
if (saved_lender) {
// handle saved case here
}
You can use query params in URI by doing:
$route.push('/', { query: { param: 3123 } })
This will generate the URL /?param=3123 and this will work after refresh. Or, you can use vuex and vuex-localstorage plugin, that let you persist data in the browser, and you can close the browser and open again and data will be restored.
In order for the state of application to persist, I recommend that you use vuex to manage the state and use a library that abstracts the persisting to vue in a clean way. One popular library is vuex-persist. https://www.npmjs.com/package/vuex-persist
If you dont want to use a store, VueSession, a package linked here should do the trick

vue-konva: removing specific node from stage working incorrectly

I'm passing down an array of image urls as props to my Konva component, creating a image object for each url, storing that object in a data object (using Vue's $set method to keep the object literal reactive), then using v-for to create a v-image for each image object in my data object. This seems to be working fine, however I'm running into a problem where if I try to remove one of the images, 2 images will be removed. This only happens if the image that I try to remove is not the topmost image. In the console, I'm getting the warning Konva warning: Node has no parent. zIndex parameter is ignored.. My hunch is that this is a result of konva's destroy method clashing with vue's $delete method on a data object used in a v-for. I've been battling with this for hours, and would appreciate any help I can get. Relevant code is below. Thanks!
Parent
<template>
<editor ref="editor" :image-props.sync="images"/>
<button #click="remove">remove</button>
</template>
export default {
components: {
Editor,
},
data() {
return {
images: [url1, url2, etc...],
};
},
methods: {
remove() {
this.$refs.editor.removeSelectedImage();
},
}
child
<template>
<div>
<v-stage>
<v-layer>
<v-group>
<v-image v-for="image in Object.values(images)"
:key="image.id" :config="image"/>
</v-group>
</v-layer>
</v-stage>
</div>
</template>
export default {
props: {
imageProps: Array,
},
data() {
return {
images: {},
selectedNode: null //this gets updated on click
},
watch: {
imageProps() {
this.registerImages();
},
mounted() {
this.registerImages();
},
methods: {
registerImages() {
this.imageProps.forEach(url => {
if (!this.images[url]) {
let img = new Image();
img.src = url;
img.onload = () => {
this.$set(this.images, url, {
image: img,
draggable: true,
name: url,
x: 0,
y: 0,
});
}
}
});
},
removeSelectedLayer() {
let newImageProps = this.imageProps.filter(url => url !== this.selectedImageName);
this.$emit('update:image-props', newImageProps);
this.selectedNode.destroy();
this.$delete(this.images, this.selectedImageName);
this.stageArea.draw();
},
If I inspect the component in Vue devtools, the images object looks correct as well as imageProps, (even the Vue DOM tree looks right with the correct amount of v-images) however the canvas shows 1 less image than it should. Again, this only happens if I remove a image that wasn't initially on top. It seems to function fine if I remove the top-most image.
When you are developing an app with vue-konva it is better not to touch Konva nodes manually (there only rare cases when you need it, like updating Konva.Transformer).
You don't need to call node.destroy() manually. Just an item for your data.
From your demo, I see you are not using key attribute (you are using image.id for that, but it is undefined). It is very important to use keys in such case.
Updated demo: https://codesandbox.io/s/30qpxpx38q

Vue test utils is updating the component data but not re-rendering the dom

I'm trying to test the template of my Vue app after making an ajax request which is changing one variable of the component' data. This variable (books) is use to conditional render the gallery
CONTEXT: I want to create a gallery in order to show the books I have stored in my back end. For this, I fetching my books on mounting the component. The result of this is set in the variable books. What I'm trying to test is that, after the ajax call, the component renders the gallery with the books
PROBLEM: When the books variable is set, the div <div v-else-if='books.length > 0'>SHOW GALLERY</div> should be rendered, but the "else" div (<div v-else class='loader'>Loading</div>) is still rendered
The next two blocks of code are the component and the test itself:
BookGallery.vue (component I'm testing)
<template>
<v-content>
<v-container fluid>
/*** Conditional rendering: After the ajax request books > 0, so this div should be rendered ***/
<div v-if='books.length > 0'>SHOW GALLERY</div>
<div v-else class='loader'>Loading</div>
</v-container>
</v-content>
</template>
<script lang='ts'>
import {Component} from 'vue-property-decorator';
import {MyMixin} from '../mixin';
#Component({components: {BookInformation}})
export default class BookGallery extends MyMixin {
public books: string[] = [];
public async mounted() {
/*** books is set as the result of the ajax request ***/
this.books = (await this.$http.get(this.host + '/books')).data;
}
}
</script>
<style scoped lang='scss'></style>
TEST
#test
public async 'after calling the books, the gallery show all of them'() {
/*** MOCKING THE RESPONSE ***/
TestCase.OK_200({
books: [
{ uri: 'img/covers/1.jpg', title: 'El Prinicipito'},
{ uri: 'img/covers/2.jpeg', title: 'The Lord of the Rings'},
],
});
/*** MOUNTING MY COMPONENT ***/
const wrapper = TestCase.shallowMount(BookGallery);
/** ASSERTING **/
await flushPromises().then(() => {
/** the first "expect" passes, so books > 0 = true **/
expect(wrapper.vm.$data.books).to.eqls({
books: [
{ uri: 'img/covers/1.jpg', title: 'El Prinicipito'},
{ uri: 'img/covers/2.jpeg', title: 'The Lord of the Rings'},
],
});
/** This is failing. The application should read 'SHOW GALLERY' when books > 0 (something tested in the previous assert), as explained in the first comment of the component's template, but is not updating the dom, only the data **/
see('SHOW GALLERY');
});
}
The QUIESTION: How can I update my DOM for my very last assert -see("SHOW GALLERY")-?
UPDATE
see Function
The function only searches for a HTML element in the wrapper that vue-test-utils is using for mounting the application. In this case, as I have left it null, it is searching the text "SHOW GALLERY" over the whole HTML file
export const see = (text: string, selector?: string) => {
const wrap = selector ? wrapper.find(selector) : wrapper;
expect(wrap.html()).contains(text);
};
I just solved a similar problem, using shallowMount and its sync: false option.
This deactivates sync rendering, requiring you to give the renderer some time to perform its job (await wrapper.vm.$nextTick()) where needed.
Once this done, the component was re-rendered based on my reactive data, as expected.
I ran into very similar problem and solved it with await wrapper.vm.$forceUpdate(); before assertion that failed. This forces Vue to update view.
Initially Vue Test Utils run updates synchronously. But later they removed sync mode. Here they explain reasons and show how to write tests:
Test code will change from this:
it('render text', (done) => {
const wrapper = mount(TestComponent)
wrapper.trigger('click')
wrapper.text().toContain('some text')
})
To this:
it('render text', async () => {
const wrapper = mount(TestComponent)
wrapper.trigger('click')
await Vue.nextTick()
wrapper.text().toContain('some text')
})
So, to fix your issue you need to add await wrapper.vm.$nextTick() before assertion see('SHOW GALLERY');
I had a similar problem where child components markup was not rendered on mount and based on the other answers the following worked for me:
test('it works as expected', async () => {
const wrapper = await mount(TestComponent);
//expect(...)
});

VueJS 2 Simplert only covers the component

A brief description is, basically when i click on a "Submit" button per say, an alert should pop out. So I'm using Simplert for Vue to do that.
The project I'm currently doing is a Single Page Application (SPA) so this is where the problem is. The alert only covers the component that I'm in and does not cover the whole page.
Problem:
Is there a way for the alert to cover the whole page?
As I see it you have at least two options to solve it: one dirty and quick and the other a little bit more design oriented
Option 1
override the css position property of the mask:
from: position: absolute to: position: fixed
Option 2
Globally declare the Simplert component, since probably you will use it a lot
// somewhere in your main/app/bootsrap.js file
Vue.compoment('simplert', require('vue2-simplert'))
Now in your root component or where you have your main <router-view> component (if you are using vue-router):
// root component
<template>
<div>
<simplert :useRadius="true" :useIcon="true" ref="simplert"/>
<router-view></router-view>
</div>
</template>
<script>
export default {
mounted () {
this.$on('trigger:simplert', (data) => this.triggerSimplert(data))
},
methods: {
triggerSimplert (data) {
this.$refs.simplert.openSimplert(data)
}
}
}
</script>
Now in any component you need to trigger the modal simply do:
...
someMethod () {
let obj = {
title: 'Custom Function',
message: 'Click close to trigger custom function',
type: 'info',
onClose: this.onClose
}
this.$emit('trigger:simplert', obj)
}
...
Hope this helps.

Rendering React via Jade (Pug)

So I'm using Jade (Pug) to render my templates via Node, and I'm trying to use React to interact with various HTML elements. Is there a special way to do this?
React (favourite-item.js)
var FavouriteItemButton = React.createClass({
getInitialState: function () {
return {favourite: false};
},
handleClick: function (event) {
this.setState({favourite: !this.state.favourite});
},
render: function() {
var text = this.state.favourite ? 'like' : 'haven\t liked';
return (
<p onClick={this.handleClick}>
You {text} this. Click to toggle.
</p>
);
}
});
RenderDOM.render(
<FavouriteItemButton />,
document.getElementById('example')
);
I've imported React/React DOM and obviously the file above. I have a HTML element on my Jade (index.jade) template:
p#example like
But nothing is changing on click, I presume it's down to the tags or some special way to render React elements via Jade.
My includes:
script(type='text/javascript' src='js/libs/react.min.js')
script(type='text/javascript' src='js/libs/react-dom.js')
script(type='text/babel' src='js/favourite-item.js')
I'm right in thinking I should include the type as text/babel?