VueJS 2 - Listen to event in mixin - vuejs2

I'm currently trying to create a mixin for Vue which basically creates a property passthrough chain. I'll clarify what should happen to be a little more clear;
Let's say I got 3 components; A,B and C.
A & B are both the same component called 'content-pane' (See below for template code).
<div class="pane-wrapper">
<div class="content-pane" :class="{'is-hidden' : !active}" :content="name">
<div class="card white">
<div class="card-title grey darken-3">
<h1 class="white-text">{{ label }}</h1>
</div>
<div class="card-content white">
<component
:is = "type"
:routes = "routes"
:passthrough = "passthrough"
keep-alive
></component>
</div>
</div>
</div>
<content-pane
v-for="(pane, key) in children"
:key = "key"
:label = "pane.label"
:name = "pane.name"
:active = "true"
:type = "pane.type"
:routes = "pane.routes"
></content-pane>
</div>
C is a dynamic component, meaning that it is interchangeable and could be any component.
Now I want to be able to access certain data from component C in component A, and for that I am trying to create a mixin that dyamically offers a data property to do this:
<script>
export default {
name: 'passthrough',
props: {
passthrough : {
type : Object
}
},
data ()
{
return {
// This object allows you to
// update the parent.
passthroughModifier : {
// We use the data object inside the
// original object because Vue doesn't
// want to detect direct prop changes
// when they are added dynamically
// into the root object...
data : {}
}
}
},
methods : {
/**
* This function fires an emit event.
*/
emitUpdate ()
{
this.$emit('passthrough-update', this.passthroughModifier);
}
},
watch : {
/**
* Emit an event once the passthrough
* property has been changed.
* We need to use a deep watcher.
*/
'passthroughModifier' : {
handler : function (val) {
this.emitUpdate();
},
deep: true
}
},
created ()
{
// Allow access to the instance
// inside the iteration.
let _that = this;
// Attach a listener for the passthrough update
// which will walk through all the keys in the
// data object and hard-set these locally.
this.$on('passthrough-update', function (data) {
Object.keys(data).forEach(function (index) {
_that.passthroughModifier[index] = data[index];
});
});
}
}
Everything works fine except listening to the 'passthrough-update' event, which is fired by the watcher on $.passthroughModifier.
So; When component C updates its $.passthroughModifier.data, the event gets emitted, but component B isn't able to catch this event.
I have tried to listen for this event in the created() method of the mixin (see code above), but it seems as if the event only gets caught in the component the event is fired from. So component C fires the event, and component C listens to its own event.
I hope someone is able to tell me wether this is actually possible or not, and what I'm doing wrong if it is possible.

Related

How to create router-link programmatically on html render using Datatables.net + 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.

Can't copy props to model data and render it in Vue 2

I'm having this problem that looks a lot like a bug to me and I can't figure out how to solve it.
I created a generic list component and I tell it what child component it should insert in each item and what are the data it should pass to the child component. I'm passing everything as props along with the list (array) itself.
The problem is that I can't mutate the list props. So I try to copy it to model attribute. Otherwise I get this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders.....
And I can't just make it work in any of the lifecycle events. When I save the file and the hot-reloading reloads the page, the list is there, rendered, full of items. When I press F5 to manually reload the page, it is no more. Everything seems to be alright with code though
So in the parent component I'm doing this:
<List ref="link_list"
:list="this.foo.links" //this is array
:child="'LinkFormItem'" //this is the name of the child component
:section_name="'Links'"
:defaults="{content: '', type: 'facebook'}" />
In the List component I get this:
Template
<li class="" v-for="item in datalist">
<component :is="child" :item="item" ></component>
<button v-on:click='remove(index++)' type="button" name="button" class='red button postfix small'>Remove</button>
</li>
Script
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
name: 'search',
props: ['child', 'list', 'defaults','section_name'], //it is received as 'list'
components: {
Child1, Child2
},
data () {
return {
index: 0,
datalist: [] //i'm trying to copy 'list' to 'datalist'
}
},
beforeMount: function () {
// i'm copying it
for(var k in this.list){
this.datalist.push(this.list[k])
}
},
methods: {
//and here I should change it the way I want
add: function () {
this.datalist.push(this.defaults)
},
getList () {
return this.datalist;
},
remove(index){
var datalist = [];
for(var k in this.datalist){
if(k != index) datalist.push(this.datalist[k]);
}
this.datalist = datalist;
}
}
}
</script>
I don't see any problems with my Script. What is going on??
#edit
Ok, some console.log later I found out what the problem seems to be. The HTTP Request is really taking much longer than the mounting of the component to happen. But when it happens, it is not triggering the update in the list component. Nothing is re-rendered and the list is empty.
Workaround
well I realised the problem was related to propagation. I made a few changes in the code to asure the parent component was updating and changing the model value. but the child component (the list component) was not receiving it.
then I gave up trying to understand why and did the following:
1- used the ref in the child component to force an update in the child component with $forceUpdate and then I was assigning the props to the model in the beforeUpdate event. It was causing an error: an re-rendering loop. The update caused a new update and so on. We could just use a flag to stop it.
2- Instead I just called a child method directly:
this.$refs.link_list.updateList(data.links);
I hate this approach because I think it's way too explicit. But it did the job. Then in the child component a new method:
updateList(list){
this.datalist = list;
}
3- The other possibility that passed through my mind was emitting an event. But I didn't try, too complicated
You can simply do like as follows
data () {
return {
index: 0,
datalist: this.list // to copy props to internal component data
}
},
Once you done about you need to apply data manipulation opertions on new this.datalist , not on this.list
If you don't want to mutate the original list array you can do this:
data () {
return {
index: 0,
datalist: Object.assign({}, this.list)
}
}
I think this will help you

How to trigger event in all sibling components except current component in vuejs?

I have a reusable component that does inline editing for data.
So a page has 10 fields that can be edited inline,
<editfield :value="field" v-for="field in fieldslist"></editfield>
Each of them have a data field called "editing" that sets as true or false as the user clicks on it. Everytime a field is set to editing an event editing-another-field is emitted using event bus.
edit(){
this.editing = true;
EventBus.$emit('editing-another-field');
}
I added the event listener when the component is created
created(){
EventBus.$on('editing-another-field', ()=>{ this.editing = false;});
}
The problem I am facing is it is triggering the event even in the currennt component being edited.
How can I mention that updated value of editing in all the other sibling components except the current component.
Why not pass the current component as an event argument and use that to check if the event originated from this component or another one.
edit() {
this.editing = true;
EventBus.$emit('editing-another-field', this);
}
created() {
EventBus.$on('editing-another-field', source => {
if (source !== this) {
this.editing = false;
}
});
}
Or you can do it like this (it is important to unregister the event listener when the component is destroyed to avoid a memory leak):
edit() {
EventBus.$emit('editing-field', this);
}
created() {
this.editingFieldHandler = vm => {
this.editing = vm === this;
};
EventBus.$on('editing-field', this.editingFieldHandler);
}
destroyed() {
EventBus.$off('editing-field', this.editingFieldHandler);
}
Otherwise you can emit the event first and then set this.editing to true.
Are you sure you want an event bus? This brings up bad memories of JQuery ;-) I think it would be cleaner to limit yourself to a tree of parents and children. Thinking MVVM, formLockedBy is a perfectly valid and sensible property to store on the parent and pass to the children.
The solution below, running here, shows a form with two fields. The fields are both instances of modal-component. The parent manages the formLockedBy property. The child fields look at this property to know to disable themselves. When the user starts typing in a field, the field emits an editing event and formLockedBy gets set. Similarly, when a field emits a save or cancel event, the parent clears formLockedBy and the other input(s) spring back to life.
Note the advantages...
Only the parent listens for events.
The identifier stored in formLockedBy is just the string name of the field. This is much safer than passing and storing a reference to the Vue component. If you don't like this, you might consider adding a safe id to the object proto.
No surprises. The full list of events the parent will react to is declared in the tag instantiating the child. The child specifies in props everything it needs from the parent.
HTML
<div id="example">
<modal-input name='First Name'
:form-locked-by='this.formLockedBy'
v-on:save='formLockedBy = null'
v-on:cancel='formLockedBy = null'
v-on:editing='fieldActive'
></modal-input>
<modal-input name='Address'
:form-locked-by='this.formLockedBy'
v-on:save='formLockedBy = null'
v-on:cancel='formLockedBy = null'
v-on:editing='fieldActive'
></modal-input>
</div>
JS
Vue.component('modal-input', {
template: `<div>
{{name}} :
<input :name='name' type="text" v-on:keydown="active" :disabled="formLockedBy && formLockedBy != name"/>
<span v-if="editing && formLockedBy == name">
<input type="button" value="Save" v-on:click="$emit('save');editing=false;"></input>
<input type="button" value="Cancel" v-on:click="$emit('cancel');editing=false;"></input>
</span>
</div>`,
data : function(){
return {editing:false};
},
props: ['name','formLockedBy'],
methods : {
active : function(event){
if(!this.editing){
this.editing = true;
this.$emit('editing',{field:this.name})
}
return true;
}
}
});
// create a root instance
new Vue({
el: '#example',
data: {
formLockedBy : null
},
methods : {
fieldActive : function(args){
this.formLockedBy = args.field;
}
}
})

Dynamically add properties to Vue component

I am loading data from the database which drives what type of component I display
An AJAX call goes off and returns me some data (this can be restructured if needed)
{
component_type: 'list-item',
props: {
name: 'test',
value: 'some value'
}
}
This is accessible on my parent object a variable called component
Within the template of my parent object I have the following
<component :is="component.component_type" ></component>
This works fine and loads the component as expected.
Next I want to add the properties from my data object into this tag too
<component :is="component.component_type" {{ component.props }} ></component>
This doesn't work and rejects writing a tag with {{ in it. I presume this is an error thrown by the browser rather than Vue, although I'm unsure.
For reference I want the output to actually look like:
<component :is="component.component_type" name='test' value='some value' ></component>
How can I go about passing in these properties? Ideally I'd like these to be tied to data / props of the parent as I'm showing so that they can easily be changed in database and the UI will change accordingly.
At worst I will generate it all on server side, but I'd rather do it via ajax as I'm currently trying to do.
In case anyone is wondering how to do this using Vue 2 you can just pass an object to v-bind:
<template>
<component :is="componentType" v-bind="props"></component>
</template>
<script>
export default {
data() {
return {
componentType: 'my-component',
props: {
foo: 'foofoo',
bar: 'barbar'
}
}
}
}
</script>
Following this thread, I see two options to do this.
one is to pass a single prop which is an object in itself, and pass all the relevant key values in it which can be used by the child component, something like following:
<component :is="component. component_type"
:options="component.props"
</component>
Other solution mentioned is to have a directive, where you pass the object and it will set the attributes which are keys in that object to corresponding values, you can see this in work here.
Vue.directive('props', {
priority: 3000,
bind() {
// set the last component child as the current
let comp = this.vm.$children[this.vm.$children.length - 1];
let values = null;
if(this._scope && this._scope.$eval) {
values = this._scope.$eval(this.expression);
} else {
values = this.vm.$eval(this.expression);
}
if(typeof values !== 'object' || values instanceof Array) {
values = { data: values };
}
// apply properties to component data
for(let key in values) {
if(values.hasOwnProperty(key)) {
let hkey = this.hyphenate(key);
let val = values[key];
if(typeof val === 'string') {
comp.$options.el.setAttribute(hkey, values[key]);
} else {
comp.$options.el.setAttribute(':' + hkey, values[key]);
}
}
}
console.log(comp.$options.el.outerHTML);
comp._initState();
},
/*
* Hyphenate a camelCase string.
*/
hyphenate(str) {
let hyphenateRE = /([a-z\d])([A-Z])/g;
return str.replace(hyphenateRE, '$1-$2').toLowerCase();
}
});
and use it like this:
<div class="app">
<component is="component.component_type" v-props="component.props"></component>
</div>
As far as I know, there is no rest props (spread props) syntax in Vue.js template.
A possible solution is render functions. You have full power of JavaScript when using render functions, so you can do something like this:
render (h) {
return h('foo', {
props: {
...yourData
}
})
}
I created a simple example here: http://codepen.io/CodinCat/pen/bgoVrw?editors=1010
both component type (like foo) and props can be dynamic (but you still need to declare all the possible fields of prop (like a, b and c) in your child components)
There is another solution is JSX.
Use the JSX babel plugin: https://github.com/vuejs/babel-plugin-transform-vue-jsx
then you can use the spread props syntax:
return <foo {...{yourData}}></foo>

Binding method result to v-model with Vue.js

How do you bind a method result to a v-model with Vue.js?
example :
<someTag v-model="method_name(data_attribute)"></someTag>
I can't make it work for some reason.
Thank you.
Years later, with more experience, I found out that is it easier to bind :value instead of using v-model. Then you can handle the update by catching #change.
Edit (per request):
<input :value="myValue" #change="updateMyValue">
...
methods: {
updateMyValue (event) {
myValue = event.target.value.trim() // Formatting example
}
}
And in a child component:
// ChildComponent.vue
<template>
<button
v-for="i in [1,2,3]">
#click="$emit('change', i) />
</template>
// ParentComponent.vue
<template>
<child-component #change="updateMyValue" />
</template>
<script>
import ChildComponent from './child-component'
export default {
components: {
ChildComponent
},
data () {
return {
myvalue: 0
}
},
methods: {
updateMyValue (newValue) {
this.myvalue = newValue
}
}
}
</script>
v-model expressions must have a get and set function. For most variables this is pretty straight forward but you can also use a computed property to define them yourself like so:
data:function(){
return { value: 5 }
},
computed: {
doubleValue: {
get(){
//this function will determine what is displayed in the input
return this.value*2;
},
set(newVal){
//this function will run whenever the input changes
this.value = newVal/2;
}
}
}
Then you can use <input v-model="doubleValue"></input>
if you just want the tag to display a method result, use <tag>{{method_name(data_attribute)}}</tag>
Agree with the :value and #change combination greenymaster.
Even when we split the computed property in get/set, which is help, it seems very complicated to make it work if you require a parameter when you call for get().
My example is a medium sized dynamic object list, that populates a complex list of inputs, so:
I can't put a watch easily on a child element, unless I watch the entire parent list with deep, but it would require more complex function to determine which of the innter props and/or lists changed and do what fromthere
I can't use directly a method with v-model, since, it works for providing a 'get(param)' method (so to speak), but it does not have a 'set()' one
And the splitting of a computed property, have the same problem but inverse, having a 'set()' but not a 'get(param)'