I am working on a table using Quasar framework's Q-Popup-edit and Vuex Store.
It populates correctly. However, when I change values on the table, it goes back to its current value and is not reflected at all.
Here is my table:
tableData: [
{
'FrozenYogurt' : {
'topping': 'strawberry'
},
'FrozenYogurtPart2' : {
'topping2': 'strawberry2'
}
},
{
'IceCreamSandwich' : {
'baseFlavor': 'chocolate',
'somethingAgain': 'chocolatiest'
}
},
{
'CreamPuff' : {
'sourceBakery': 'Starbucks'
}
}
]
My Vuex mutation:
mutations: {
saveUpdatedData (newVal) {
console.log('inside MUTATION saveUpdatedData')
state.tableData.length = 0
state.tableData.push(newVal)
}
}
And using a two-way computed property (get/set) to populate the table:
tableRows: {
get: function () {
console.log('inside GET')
return this.$store.state.tableData.reduce((acc, item) => {
Object.keys(item).forEach(name => {
Object.keys(item[name]).forEach(property => {
acc.push({ name, property, value: item[name][property]})
})
})
return acc
}, [])
},
set: function (newValue) {
console.log('inside SET')
this.$store.commit('saveUpdatedData', newValue)
}
}
But the set() function isn't being called at all.
And finally my Vue code:
<q-table
:data="tableRows"
:columns="columns"
:rows-per-page-options="[]"
row-key="name" wrap-cells>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="desc" :props="props">
{{ props.row.name }}
<q-popup-edit v-model="props.row.name" buttons>
<q-input v-model="props.row.name" dense autofocus counter ></q-input>
</q-popup-edit>
</q-td>
<q-td key="property" :props="props">
{{ props.row.property }}
<q-popup-edit buttons v-model="props.row.property">
<q-input type="textarea" v-model="props.row.property" autofocus counter #keyup.enter.stop></q-input>
</q-popup-edit>
</q-td>
<q-td key="value" :props="props">
{{ props.row.value }}
<q-popup-edit v-model="props.row.value" buttons>
<q-input v-model="props.row.value" dense autofocus ></q-input>
</q-popup-edit>
</q-td>
</q-tr>
</template>
</q-table>
How can I make the changes reflect on the vuex store??
CodePen here:
https://codepen.io/kzaiwo/pen/BaNYbZZ?editors=1011
Help!
You can extend your computed property by using computed getter and setter:
https://v2.vuejs.org/v2/guide/computed.html#Computed-Setter
Your existing computation code moves to the get function.
The set function needs to be implemented.
You can refer to this url https://vuex.vuejs.org/guide/mutations.html
You will find the correct way to mutate the vuex store.
I am afraid that you still have to use data prop like in the docs. (I have tried computed property setter but it wouldn't work)
To integrate it with the store you can use the following solution: https://codepen.io/woothu/pen/VwLXzNQ?editors=1011
data () {
return {
columns: [
{ name: 'desc', align: 'left', label: 'Data Element', field: 'desc', sortable: true, style: 'min-width: 180px; max-width: 180px;' },
{ name: 'property', align: 'center', label: 'Property', field: 'property', sortable: true, style: 'min-width: 20px; max-width: 20px;' },
{ name: 'value', align: 'left', label: 'Value', field: 'value', sortable: true, style: 'min-width: 300px; max-width: 300px; word-wrap: break-word; font-family: Consolas; font-size: 13px' }
],
tableRows: null
}
},
beforeMount () {
this.tableRows = this.$store.state.tableData.reduce((acc, item) => {
Object.keys(item).forEach(name => {
Object.keys(item[name]).forEach(property => {
acc.push({ name, property, value: item[name][property]})
})
})
return acc
}, [])
},
watch: {
tableRows: {
deep: true,
handler (val) {
this.$store.commit('saveUpdatedData', val)
}
}
}
Btw. I think the QTable component should emit the event when it changes data with the updated data structure. (Or make some other functionality to enable handling Vuex) It is probably worth opening the PR with mention of this discussion.
Related
I hope you could help me out.
Before going through the code, let me quickly explain what I want:
I have two components that I use for uploading and displaying images. I have FileResourceService that is used for uploading, and FileResourceImage which is used for storing and displaying the data. These work together with a v-model called profilePictureFileResourceId which basically just ties the images to specific users on the page, depending on who is logged on.
When displaying the image on a template, it is very straightforward. I just grab the FileResourceImage component and tie it with the v-model.
<file-resource-image v-model="form.user.profilePictureFileResourceId" can-upload style="width: 100px; height: 100px;" />
That is all very easy, but I have some pages where I use tables that contain information about my users, and I would like for the user’s profile images to actually be displayed in the table. Here is an example of a list used for the table.
fields() {
return [
{
key: "email",
label: this.$t('email'),
sortable: true,
template: {type: 'email'}
},
{
key: "name",
label: this.$t('name'),
sortable: true
},
{
key: 'type',
label: this.$t('type'),
formatter: type => this.$t(`model.user.types.${type}`),
sortable: true,
sortByFormatted: true,
filterByFormatted: true
},
{
key: 'status',
label: this.$t('status'),
formatter: type => this.$t(`model.user.status.${type}`),
sortable: true,
sortByFormatted: true,
filterByFormatted: true
},
{
key: "actions",
template: {
type: 'actions',
head: [
{
icon: 'fa-plus',
text: 'createUser',
placement: 'left',
to: `/users/add`,
if: () => this.$refs.areaAuthorizer.fullControl
}
],
cell: [
{
icon: 'fa-edit',
to: data => `/users/${data.item.id}/edit`
}
]
}
I know that I cannot just make an array that looks like this:
fields() {
return [
{
<file-resource-image v-model="form.user.profilePictureFileResourceId" can-upload />
}
]
}
So how would you make the component display from within in the list? I believe it can be done with props, but I am totally lost at what to do.
By the way, these are the two components I use for uploading and display. I thought I might as well show them, so you can get an idea of what they do.
For upload:
import axios from '#/config/axios';
import utils from '#/utils/utils';
export const fileResourceService = {
getFileResource(fileResourceId) {
return axios.get(`file/${fileResourceId}`);
},
getFileResourceFileContent(fileResourceId) {
return axios.get(`file/${fileResourceId}/download`, {responseType: 'arraybuffer', timeout: 0});
},
downloadFileResource(fileResourceId) {
return fileResourceService.getPublicDownloadToken(fileResourceId)
.then(result => fileResourceService.downloadPublicTokenFile(result.data));
},
downloadPublicTokenFile(fileResourcePublicDownloadTokenId) {
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href =
`${axios.defaults.baseURL}/file/public/${fileResourcePublicDownloadTokenId}/download`;
tempLink.setAttribute('download', '');
document.body.appendChild(tempLink);
tempLink.click();
setTimeout(() => document.body.removeChild(tempLink), 0);
},
getPublicDownloadToken(fileResourceId) {
return axios.get(`file/${fileResourceId}/public-download-token`);
},
postFileResource(fileResource, file) {
return axios.post(`file`, utils.toFormData([
{name: 'fileResource', type: 'json', data: fileResource},
{name: 'file', data: file}
]), {timeout: 0});
}
};
Then we have the component that is used for DISPLAYING the images:
<template>
<div :style="style" #click="upload" style="cursor: pointer;">
<div v-if="url === null">
<i class="fas fa-camera"></i>
</div>
<div v-if="canUpload" class="overlay">
<i class="fas fa-images"></i>
</div>
</div>
</template>
<script>
import {fileResourceService} from '#/services/file-resource';
import utils from '#/utils/utils';
export default {
model: {
prop: 'fileResourceId',
event: 'update:fileResourceId'
},
props: {
fileResourceId: String,
canUpload: Boolean,
defaultIcon: {
type: String,
default: 'fas fa-camera'
}
},
data() {
return {
url: null
};
},
computed: {
style() {
return {
backgroundImage: this.url && `url(${this.url})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
};
}
},
methods: {
upload() {
if(this.canUpload) {
utils.openFileDialog()
.then(([file]) => fileResourceService.postFileResource({}, file))
.then(result => this.$emit('update:fileResourceId', result.data.id))
.catch(() => this.$bvModalExt.msgBoxError())
}
}
},
watch: {
fileResourceId: {
immediate: true,
handler() {
this.url = null;
if (this.fileResourceId) {
fileResourceService.getFileResourceFileContent(this.fileResourceId).then(result => {
const reader = new FileReader();
reader.onload = event => this.url = event.target.result;
reader.readAsDataURL(new Blob([result.data]));
});
}
}
}
}
};
</script>
I'm trying to have a TypeScript variable bind to the translate service just like binding in the HTML markup, which works fine.
Here's what I've tried so far
ngOnInit() {
this.customTranslateService.get("mainLayout.userProfileDropdown.changeLocale").subscribe((result) => {
this.changeLocaleText = result;
})
this.customTranslateService.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.changeLocaleText = this.customTranslateService.instant("mainLayout.userProfileDropdown.changeLocale");
});
this.userProfileMenuOptions = [
{
text: this.changeLocaleText, itemId: "LocaleSelect"
},
{
text: "Report a bug", itemId: "BugReport"
},
{
text: "Request a feature", itemId: "FeatureRequest"
},
{
text: "Log Out", itemId: "LogOut"
}
];
}
customTranslateService is just a service that wraps TranslateService.
The first subscription works ok, but when I switch languages, the onLangChange does trigger, changing the variable content correctly, but userProfileMenuOptions's reference to changeLocaleText is not binded therefore not updated.
Using a BehaviorSubject can't really be done here as it is typescript code, not html markup that can use the async pipe.
Maybe recreating the userProfileMenuOptions array everytime the language change subscription is called could be an option, although I'm not sure the component that uses the array will like it.
PS: instant will work here because I have an application loader that loads all available languages before the application is available to the user.
Any ideas ?
ngOnInit() {
this.customTranslateService.get("mainLayout.userProfileDropdown.changeLocale").subscribe((result) => {
this.changeLocaleText = result;
})
const getUserPorfileMenuOptions = (changeLocaleText: string) => {
return [
{
text: this.changeLocaleText, itemId: "LocaleSelect"
},
{
text: "Report a bug", itemId: "BugReport"
},
{
text: "Request a feature", itemId: "FeatureRequest"
},
{
text: "Log Out", itemId: "LogOut"
}
];
}
this.customTranslateService.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.changeLocaleText = this.customTranslateService.instant("mainLayout.userProfileDropdown.changeLocale");
this.userProfileMenuOptions = getUserPorfileMenuOptions(this.changeLocaleText);
});
this.userProfileMenuOptions = getUserPorfileMenuOptions(this.changeLocaleText);
}
I am adding styles when registering my block:
styles: [
{ name: "my-style-1", label: "Style Name" }
{ name: "my-style-2", label: "Style Name 2" }
],
In the edit() and save() function how can I see which style/classname was selected?
I tried for example:
edit( { attributes, setAttributes, styles } ) {
const blockProps = useBlockProps();
const { quote, name, title } = attributes;
console.log(styles);
console.log(blockProps.styles);
...
But it returns undefined.
I need to use the styles for conditions for example...
if (style == 'my-style-1') {
// do something if style 1 was selected
}
The selected Block Style name as defined in your styles[{...}] is available in the edit() function as className:
edit({ attributes, setAttributes, className}) {
console.log(className);
...
}
I'd suggest if you want to reorder elements based on their style, create Block Styles and use CSS flexbox to manage the reordering, eg display:flex for your wrapper div and order: ... for the child elements (like <img> and <p>). By using styles, when the content is saved the underlying HTML markup doesn't change so less change of getting the dreaded 'block validation' error (plus you get a preview of the style in the Editor). Make sure to save blockProps in the save() so the selected class is applied, eg:
edit({ attributes, setAttributes, className }) {
const blockProps = useBlockProps();
console.log(className);
return (
<div {...blockProps}>
<h2>Hello</h2><img />
</div>
);
},
save({ attributes }) {
const blockProps = useBlockProps.save();
return (<div {...blockProps}><h2>Hello</h2><img /></div>)
}
The generated class applied to the <div> will be .wp-block-myblock-name .is-style-my-style-1
I would recommend you to use Block Variations instead of Block Styles. When creating a variation you can assign attribute values.
For example:
index.php
registerBlockType('xy/yourBlock', {
title: 'xy',
description: 'xy',
attributes: {
quote: {
type: 'string'
},
name: {
type: 'string'
},
title: {
type: 'string'
},
style: {
type: 'string'
}
},
variations: [
{
name: 'my-style-1',
isDefault: true,
title: 'Style Name',
attributes: { style: 'my-style-1' },
scope: 'transform',
},
{
name: 'my-style-2',
title: 'Style Name 2',
attributes: { style: 'my-style-2' },
scope: 'transform',
},
],
})
With scope: 'transform' you can select your variation in the block settings on the right side. Once a variation is selected you can access it in your edit and save file like any other attribute.
edit( { attributes, setAttributes } ) {
const { quote, name, title, style } = attributes;
console.log(style);
if (style == 'my-style-1') {
// do something if style 1 was selected
}
i have implemented the ag-grid-vue on my project now i have a seperate component on one of the columns which is basically Actions , now the user can either edit view or delete depending on the selection, now for edit and delete it works just fine, the problem is when i am deleting a record i want the table to be re-rendered by fetching the updated data from the Api, for that i need to call some method in the parent, from the CellRenderer Component, let me show you the code
HTML
<ag-grid-vue
ref="agGridTable"
:components="components"
:gridOptions="gridOptions"
class="ag-theme-material w-100 my-4 ag-grid-table"
:columnDefs="columnDefs"
:defaultColDef="defaultColDef"
:rowData="accounts"
rowSelection="multiple"
colResizeDefault="shift"
:animateRows="true"
:floatingFilter="true"
:pagination="true"
:paginationPageSize="paginationPageSize"
:suppressPaginationPanel="true"
:enableRtl="$vs.rtl">
</ag-grid-vue>
JS
import CellRendererActions from "./CellRendererActions.vue"
components: {
AgGridVue,
vSelect,
CellRendererActions,
},
columnDefs: [
{
headerName: 'Account ID',
field: '0',
filter: true,
width: 225,
pinned: 'left'
},{
headerName: 'Account Name',
field: '1',
width: 250,
filter: true,
},
{
headerName: 'Upcoming Renewal Date',
field: '2',
filter: true,
width: 250,
},
{
headerName: 'Business Unit Name',
field: '3',
filter: true,
width: 200,
},
{
headerName: 'Account Producer',
field: '4',
filter: true,
width: 200,
},
{
headerName: 'Actions',
field: 'transactions',
width: 150,
cellRendererFramework: 'CellRendererActions',
},
],
components: {
CellRendererActions,
}
CellRenderer Component
<template>
<div :style="{'direction': $vs.rtl ? 'rtl' : 'ltr'}">
<feather-icon icon="Edit3Icon" svgClasses="h-5 w-5 mr-4 hover:text-primary cursor-pointer" #click="editRecord" />
<feather-icon icon="EyeIcon" svgClasses="h-5 w-5 mr-4 hover:text-danger cursor-pointer" #click="viewRecord" />
<feather-icon icon="Trash2Icon" svgClasses="h-5 w-5 hover:text-danger cursor-pointer" #click="confirmDeleteRecord" />
</div>
</template>
<script>
import { Auth } from "aws-amplify";
import { API } from "aws-amplify";
export default {
name: 'CellRendererActions',
methods: {
async deleteAccount(accountId) {
const apiName = "hidden";
const path = "/hidden?id="+accountId;
const myInit = {
headers: {
Authorization: `Bearer ${(await Auth.currentSession())
.getIdToken()
.getJwtToken()}`
}
};
return await API.get(apiName, path, myInit);
},
viewRecord(){
this.$router.push("/accounts/" + this.params.data[0]).catch(() => {})
},
editRecord() {
// console.log(this.params.data);
this.$router.push("hidden" + this.params.data[0]).catch(() => {})
/*
Below line will be for actual product
Currently it's commented due to demo purpose - Above url is for demo purpose
this.$router.push("hidden" + this.params.data.id).catch(() => {})
*/
},
confirmDeleteRecord() {
this.$vs.dialog({
type: 'confirm',
color: 'danger',
title: `Confirm Delete`,
text: `You are about to delete "${this.params.data[1]}"`,
accept: this.deleteRecord,
acceptText: "Delete"
})
},
deleteRecord() {
/* Below two lines are just for demo purpose */
this.$vs.loading({ color: this.colorLoading });
this.deleteAccount(this.params.data[0]).then(() => {
this.$vs.loading.close();
this.showDeleteSuccess()
});
/* UnComment below lines for enabling true flow if deleting user */
// this.$store.dispatch("userManagement/removeRecord", this.params.data.id)
// .then(() => { this.showDeleteSuccess() })
// .catch(err => { console.error(err) })
},
showDeleteSuccess() {
this.$vs.notify({
color: 'success',
title: 'User Deleted',
text: 'The selected user was successfully deleted'
})
}
}
}
</script>
now the component above is where i need to make the changes, i tried to use the reqgular vuejs emit and on but that didnt work any help?
2 ways to solve this -
1. cellRendererParams approach
You can use cellRendererParams like this -
cellRendererParams : {
action : this.doSomeAction.bind(this); // this is your parent component function
}
Now in your cell renderer component you can invoke this action
this.params.action(); // this should correspond to the object key in cellRendererParam
2. Using context gridOption
There is another way to solve this as described in this example
You basically setup context in your main grid component like this -
:context="context" (in template)
this.context = { componentParent: this };
Then in your component you can call parent component like this -
invokeParentMethod() {
this.params.context.componentParent.methodFromParent(
`Row: ${this.params.node.rowIndex}, Col: ${this.params.colDef.headerName}`
);
}
In my case #click event is being removed automatically.
Am I missing something?
<button #click="editRecord" >Click Me</button>
Actual Output:
<button >Click Me</button>
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.