Vue.js Event passthrough triggering 'click' event and named event when I only want the named event - vue.js

I'm using the VueFormulate library to generate forms from a JSON schema. Doing this I would like the ability to add any event listeners I need without having to update my generic component so I'm attempting to pass the event up using v-on="$listeners". The trouble is that I need to do some processing before passing the form object back to the client so I need to have some events specified.
For example, below when clicking the button that emits the 'save' event, the parent component ends up receiving the 'save' event but also a 'click' event. Both of which trigger the 'save' event listeners. If I remove v-on="$listeners" then the defined events work, however those specified only in the schema no longer get passed through.
// Dynamic-form.vue
<template>
<div>
{{ $listeners }}
<FormulateForm
:name="name"
v-model="theForm"
:schema="filteredFormSchema"
#submit="handleSubmit"
#save="handleSave"
v-on="$listeners"
>
</FormulateForm>
</div>
</template>
<script>
import "./vue-formulate-config"
import { cloneDeep } from "lodash";
export default {
name: "DynamicForm",
props: {
formSchema: {
type: Array,
required: true,
},
formData: {
type: Object,
required: true,
},
name: {
type: String,
default: 'theForm'
},
hideOptionalFields: {
type: Boolean,
default: false
}
},
data() {
return {
theForm: {},
schema: [],
};
},
methods: {
handleSave: function() {
this.$emit("save", this.generateFormData(this.theForm))
},
handleSubmit: function (formValues) {
this.$emit("submit", this.generateFormData(formValues));
},
},
};
</script>
Below is the snippet using the component above. You can see I'm listening for some events that are not emitted directly from that component. Those are specified in the schema json and are passed through.
// some-form.vue
<DynamicForm
class="nested-alt p-10 rounded"
name="some-form"
:formSchema="k209Schema"
:formData="form"
#submit="handleSubmit"
#save="saveAsDraft($event)"
#showPreview="handleShowPreview"
#cancel="handleCancelClick"
></DynamicForm>
[
{
name: 'preview',
type: 'button',
label: 'Preview',
"input-class": 'btn btn-outline-dark btn-sm',
'#click': 'showPreview'
},
{
name: 'cancel',
type: 'button',
label: 'Cancel',
"input-class": 'btn btn-outline-dark btn-sm',
'#click': 'cancel'
},
{
name: 'save',
type: 'button',
label: 'Save as Draft',
"input-class": 'btn btn-outline-dark btn-sm',
'#click': 'save'
},
{
name: 'submit',
type: 'submit',
label: 'Send',
"input-class": 'btn btn-info btn-sm'
}
]

When doing this you have to be careful with the event names chosen. In this case the event name in the schema is the same event name being emitted from the handleSave() method. So what's happening is the 'save' event from VueFormulate is caught in dynamic-form.vue where #save catches it and calls that method. Additionally, the v-on="$listeners" is taking that same re-named click event (i.e. the 'save' click event) and passing it through. Now you have a save event containing the click payload as well as a save event contining your form payload from the handleSave() method.
The fix: change the 'save' event name in one of the places. I ended up changine the schema click to be more explicit that it's a click and named it save-click.
// some-form.vue
<DynamicForm
class="nested-alt p-10 rounded"
name="some-form"
:formSchema="k209Schema"
:formData="form"
#submit="handleSubmit"
#save-click="saveAsDraft($event)"
#showPreview="handleShowPreview"
#cancel="handleCancelClick"
></DynamicForm>
[
{
name: 'preview',
type: 'button',
label: 'Preview',
"input-class": 'btn btn-outline-dark btn-sm',
'#click': 'showPreview'
},
{
name: 'cancel',
type: 'button',
label: 'Cancel',
"input-class": 'btn btn-outline-dark btn-sm',
'#click': 'cancel-click'
},
{
name: 'save',
type: 'button',
label: 'Save as Draft',
"input-class": 'btn btn-outline-dark btn-sm',
'#click': 'save-click'
},
{
name: 'submit',
type: 'submit',
label: 'Send',
"input-class": 'btn btn-info btn-sm'
}
]

Related

Cannot read properties of undefined on formatter, but data is showing fine

I have a bootstrap table that shows a list of appliances. I am importing my data with Axios and for this specific table I am outputting data from two database tables, so I have one object which is called applianceReferences which stores another object called activeAppliances.
Not sure if it is relevant for this question, but just so you know.
Before talking about the problem, let me just post the whole code and below I will talk about the section that is giving me issues.
<template>
<b-container class="my-2">
<b-card v-if="showTable" class="ml-4 mr-4">
<b-table
search-placeholder="search"
:filter-included-fields="fields.map(f => f.key)"
include-filter
:items="applianceReferences"
:fields="fields"
/>
</b-card>
</b-container>
</template>
<script>
import {applianceService} from "#/services/appliance";
import CommonCollapsible from "#/components/common/CommonCollapsible";
import moment from 'moment';
export default {
components: { CommonCollapsible, CommonTable },
props: {
ownerId: String,
ownerType: String,
showDocuments: Boolean,
goToAppliances: "",
importAppliances: ""
},
data() {
return {
applianceReferences: [],
showTable: true
}
},
computed: {
fields() {
return [
{
key: 'referenceName',
label: this.$t('referenceName'),
sortable: true
},
{
key: 'activeAppliance.type',
label: this.$t('type'),
sortable: true,
},
{
key: 'activeAppliance.brandName',
label: this.$t('brand'),
sortable: true
},
{
key: 'activeAppliance.purchaseDate',
label: this.$t('purchaseDate'),
sortable: true,
template: {type: 'date', format: 'L'}
},
{
key: 'activeAppliance.warrantyDuration',
label: this.$t('warrantyDuration'),
sortable: true,
formatter: (warrantyDuration, applianceId, appliance) =>
this.$n(warrantyDuration) + ' ' +
this.$t(appliance.activeAppliance.warrantyDurationType ?
`model.appliance.warrantyDurationTypes.${appliance.activeAppliance.warrantyDurationType}` :
''
).toLocaleLowerCase(this.$i18n.locale),
sortByFormatted: (warrantyDuration, applianceId, appliance) =>
appliance.activeAppliance.warrantyDurationType === 'YEARS' ? warrantyDuration * 12 : warrantyDuration
},
{
key: 'activeAppliance.purchaseAmount',
label: this.$t('amount'),
sortable: true,
template: {
type: 'number', format: {minimumFractionDigits: '2', maximumFractionDigits: '2'},
foot: sum
}
},
{
key: 'actions',
template: {
type: 'actions',
head: [
{
text: 'overviewOfAppliances',
icon: 'fas fa-fw fa-arrow-right',
action: this.createAppliance
},
{
icon: 'fas fa-fw fa-file-excel',
action: this.importAppliance,
tooltip: this.$t('importAppliances'),
}
],
cell: [
{
icon: 'fa-trash',
variant: 'outline-danger',
action: this.remove
},
]
}
}
]
},
},
methods: {
load() {
Object.assign(this.$data, this.$options.data.apply(this));
this.applianceReferences = null;
applianceService.listApplianceReferences(this.ownerId).then(({data: applianceReferences}) => {
this.applianceReferences = applianceReferences;
this.applianceReferences.forEach( reference => {
applianceService.listAppliances(reference.id).then(result => {
this.$set(reference, 'appliances', result.data);
this.$set(reference, 'activeAppliance', result.data.find(appliance => appliance.active))
this.loaded = true
})
})
}).catch(error => {
console.error(error);
})
},
createAppliance(){
this.goToAppliances()
},
importAppliance(){
this.importAppliances()
},
},
watch: {
ownerId: {
immediate: true,
handler: 'load'
}
},
}
</script>
Okay, so the error occurs in this specific property:
{
key: 'activeAppliance.warrantyDuration',
label: this.$t('warrantyDuration'),
sortable: true,
formatter: (warrantyDuration, applianceId, appliance) =>
this.$n(warrantyDuration) + ' ' +
this.$t(appliance.activeAppliance.warrantyDurationType ?
`model.appliance.warrantyDurationTypes.${appliance.activeAppliance.warrantyDurationType}` :
''
).toLocaleLowerCase(this.$i18n.locale),
sortByFormatted: (warrantyDuration, applianceId, appliance) =>
appliance.activeAppliance.warrantyDurationType === 'YEARS' ? warrantyDuration * 12 : warrantyDuration
},
What I am basically doing here is combining two values from the object: warrantyDuration and warrantyDurationType and putting them in one single row in my bootstrap table.
The problem is that this is giving me an error: Cannot read properties of undefined (reading 'warrantyDurationType'
Yet the data actually outputs normally.
So what exactly does it want me to do?
I tried wrapping a v-if around the table to make sure that the application checks if the data exist before outputting it, but this does not solve the issue.
<div v-if="applianceReferences && applianceReferences.activeAppliance">
<b-card v-if="showTable" class="ml-4 mr-4">
<common-table
search-placeholder="search"
:filter-included-fields="fields.map(f => f.key)"
include-filter
:items="applianceReferences"
:fields="fields"
/>
</b-card>
</div>
Last, just to give you a full overview, my array looks like this:
Any ideas?

Vue Good Table how to access data in Selected Row Actions

I'm trying to use the checkbox in vue-good-table to select rows, then a button in the selected row action slow to perform a function on the selected rows. How can I access the data?
https://xaksis.github.io/vue-good-table/guide/advanced/checkbox-table.html#selected-row-action-slot
This doesn't work:
<vue-good-table
#on-selected-rows-change="selectAll"
:columns="columns2"
id="shift.id"
:ref="shift.id"
:rows="orderedPlacedUsers(shift)"
:select-options="{
enabled: true,
selectOnCheckboxOnly: true,
}"
>
<div slot="selected-row-actions">
<button class="btn btn__small btn__flat" #click="lockAll(shift)">Lock All <i class="ml-2 fas fa-lock-alt"></i></button>
</div>
</div>
</vue-good-table>
Then
data() {
return {
columns2: [
{
label: '',
field: 'extras',
tdClass: 'text-center',
sortable: false,
},
{
label: 'Name',
field: 'fullName',
},
{
label: 'Signed Up',
field: 'created',
sortable: false,
},
{
label: 'Job',
field: 'requestedJob.title',
tdClass: 'text-center',
},
{
label: '',
field: 'notes',
sortable: false,
tdClass: 'text-center',
},
{
label: '',
field: 'reservations',
tdClass: 'text-center',
tdClass: 'text-center',
sortable: false,
},
{
label: '',
field: 'delete',
tdClass: 'text-center',
sortable: false,
},
]
}
}
methods: {
lockAll(shift) {
console.log(shift.id)
console.log(this.$refs[shift.id].selectedRows)
},
orderedPlacedUsers (shift) {
function compare(a, b) {
if (a.firstName < b.firstName)
return -1;
if (a.firstName > b.firstName)
return 1;
return 0;
}
return this.filteredPlacedUsers.sort(compare).filter(user => {
return user.shift == shift.id && user.day == shift.day
});
},
}
The shift is "shift in eventShifts"... here's what that looks like:
{"day":"2021-08-27","endTime":null,"payrollComplete":true,"startTime":"14:00","event":"Los Bukis","id":"dvaBm5wQXMXvVCGBSK8e","exportedCont":{"seconds":1631208172,"nanoseconds":886000000},"collapse":false,"position":{"title":null},"selectedStaff":null,"eventId":"CGHMVzcKPnNLsmRxoeVj","exportedEmp":{"seconds":1631208185,"nanoseconds":622000000},"name":"Line Cook","staff":"50"}
Thank you!
To access the vue-good-table via this.$refs you need to add :ref property into vue-good-table.
<vue-good-table
:id="shift.id"
:ref="shift.id"
>
</vue-good-table>
But there is another thing to be considered, it says here that
When used on elements/components with v-for, the registered reference
will be an Array containing DOM nodes or component instances.
In your case, probably vue-good-table is used on an element/component with v-for. So, you can access it via this.$refs[shift.id][0]. Finally, you can print the selectedRows using console.log(this.$refs[shift.id][0].selectedRows)

How to set dynamic v-model for input and set value to this v-model in Vue 2

I have a component for creating dynamic input fields. I have a prop - fields.
fields: [
{
label: 'Question Text',
placeholder: 'Enter Text',
isRequired: true,
editable: true,
name: 'question',
value: '123',
},
{
label: 'Button Text',
placeholder: 'Enter Text',
isRequired: true,
editable: true,
name: 'button',
value: '',
},
],
Than I pass this to component SettingsViewFields.vue
<template>
<div>
<e-form-group v-for="(field, index) in fields" :key="index" :error="getInputError[field.name]">
<template v-slot:label>
{{ field.label }}
</template>
<e-textarea
v-model="form[field.name]"
:name="field.name"
size="small"
#input="onInput($event, field.name)"
></e-textarea>
</e-form-group>
</div>
</template>
<script>
export default {
name: 'SettingsViewFields',
props: {
fields: {
type: Array,
default: () => [],
},
},
data: () => ({
form: [],
}),
};
</script>
I want that reactive data for input calls from field.name that the reactive data will be form: {question: '', button: ''} But the problem I have to set correct value for input, but if I set field.name the value will be not correct because for first input it's value: '123'.
You can't Change the data passed by the parent component directly, when you receive the prop data from parent that you want update, you can make a copy of this,eg
computed: {
fieldsCopy: function () {
return this.fields
},
}
and use fieldsCopy in your template, that may work.
if you also want to pass the updated data to parent , use emit.

How to trigger a Vue method by name for each element in a v-for loop?

I'm having troubles triggering a separate Vue instance method by name for each element in a v-for loop on click.
Each action corresponds to a method, but it's not triggered. What am I doing wrong?
Code:
<v-btn v-for="btn in windowControlButtons" :key="btn.id"
#click="btn.action"
>
<v-icon size="20px">{{btn.icon}}</v-icon>
</v-btn>
...
window: remote.getCurrentWindow(),
windowControlButtons: [
{
icon: 'remove',
action: minimizeWindow()
},
{
icon: 'crop_square',
action: maximizeWindow()
},
{
icon: 'close',
action: closeWindow()
}
]
...
methods: {
minimizeWindow() {
this.window.minimize()
},
maximizeWindow() {
this.window.maximize()
},
closeWindow() {
this.window.close()
}
}
UPDATE
I can trigger some code directly in the data(), e.g.:
...
{
icon: 'remove',
action: () => {remote.getCurrentWindow().minimize()}
},
But what if a method wasn't as short?
How do I trigger a method already specified in methods: { }?
btn.action is a string, thus you can't execute it.
Every Vue instance/component method is accessible as a property in the vm.$options.methods object.
I suggest creating another method, say handleClick, to simplify your method calling depending on the button, and invoke the best suitable method from this.$options.methods as shown below.
new Vue({
el: '#app',
data: {
windowControlButtons: [
{id: 1, icon: 'remove', action: 'minimizeWindow'},
{id: 2, icon: 'crop_square', action: 'maximizeWindow'},
{id: 3, icon: 'close', action: 'closeWindow'}
]
},
methods: {
handleClick(button) {
if (this.$options.methods[button.action]) { // guard to prevent runtime errors
this.$options.methods[button.action]();
}
},
minimizeWindow() {
console.log('minimizeWindow');
},
maximizeWindow() {
console.log('maximizeWindow');
},
closeWindow() {
console.log('closeWindow');
}
}
})
<script src="https://unpkg.com/vue#2.5.15/dist/vue.min.js"></script>
<div id="app">
<button v-for="btn in windowControlButtons" :key="btn.id" #click="handleClick(btn)">
<span>{{btn.icon}}</span>
</button>
</div>

angular primeng how to display context menu on left click

i am new to angular2/primeng.
How to show the primeng context menu on left click instead of the default right click inside a datatable?
thanks
Since version 6.1.4 of PrimeNG it can be done with triggerEvent property.
On the template:
<img #targetImage2 src="assets/img/vejo_trabalhos_bonitos.jpg" alt="Test image">
<p-contextMenu [target]="targetImage2" [model]="contextMenuItems" [appendTo]="'body'" triggerEvent="click"></p-contextMenu>
On the component:
contextMenuItems: MenuItem[];
constructor(private messageService: MessageService) { }
ngOnInit(): void {
this.contextMenuItems = [
{ label: 'Edit', icon: 'pi pi-fw pi-pencil', command: event => this.displayMessage(event.item.label) },
{ label: 'Export', icon: 'pi pi-fw pi-external-link', command: event => this.displayMessage(event.item.label) },
{ label: 'Delete', icon: 'pi pi-fw pi-trash', command: event => this.displayMessage(event.item.label) }
];
}
displayMessage(action: string): void {
this.messageService.add({severity: 'info', summary: `"${action}" action clicked!`});
}
But if one wants to display the context menu on both left and right mouse button events. It can be done too.
On the template:
<img #targetImage src="assets/img/vejo_trabalhos_bonitos.jpg" alt="Test image" (click)="onLeftMouseClick($event,contextMenu)">
<p-contextMenu #contextMenu [target]="targetImage" [model]="contextMenuItems" [appendTo]="'body'"></p-contextMenu>
On the component:
contextMenuItems: MenuItem[];
constructor(private messageService: MessageService) { }
ngOnInit(): void {
this.contextMenuItems = [
{ label: 'Edit', icon: 'pi pi-fw pi-pencil', command: event => this.displayMessage(event.item.label) },
{ label: 'Export', icon: 'pi pi-fw pi-external-link', command: event => this.displayMessage(event.item.label) },
{ label: 'Delete', icon: 'pi pi-fw pi-trash', command: event => this.displayMessage(event.item.label) }
];
}
displayMessage(action: string): void {
this.messageService.add({severity: 'info', summary: `"${action}" action clicked!`});
}
onLeftMouseClick(event: MouseEvent, contextMenu: ContextMenu): void {
event.stopPropagation();
event.preventDefault();
contextMenu.show(event);
}
You might want to reconsider why you would want to change the default like that. Right clicking is pretty much the universal way that people expect Context Menus to work. As Pardeep said, other than rooting around in the code, you wouldn't be able to change that.