I am learning Vue and Grpahql, and have created a Trello like application.
I have created a page, that calls a mutation and adds a card entry to the database. In order to refresh the page I am trying to clear the query cache. When you click the add card button it should appear in the list. It happens the first time you click add card button, but not on subsequent tries.
<template>
<button
#click="addCard"
class="flex w-full items-center pl-2 pr-4 py-2 text-sm font-medium text-gray-800 bg-gray-100 rounded-md hover:bg-gray-200"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
<span class="ml-1">Add card</span>
</button>
</template>
<script>
import CardAdd from "./../graphql/CardAdd.gql";
import BoardQuery from "./../graphql/BoardWithListsAndCards.gql";
export default {
methods: {
addCard() {
this.$apollo.mutate({
mutation: CardAdd,
variables: {
title: 'Added through mutation',
listId: 1,
order: 1
},
update(store, {data: {cardAdd} }) {
const data = store.readQuery({
query: BoardQuery,
variables: { id: 1 }
});
data.board.lists.find(list => (list.id = 1)).cards.push(cardAdd);
store.writeQuery({query: BoardQuery, data });
}
});
}
}
}
</script>
This is the add card mutation:
mutation CardAdd($listId: ID!, $title: String!, $order: Int!) {
cardAdd(
input: {
title: $title
list_id: $listId
order: $order
owner_id: 1
}
) {
id
title
order
}
}
This is the board query:
query BoardWithListsAndCards($id: ID!) {
board(id: $id) {
id
title
color
lists {
id
title
board_id
board {
owner {
id
}
}
cards {
id
title
order
owner {
id
}
}
}
}
}
Related
I have a form with two checkboxes. each of them are related to an object to change its props 'is_active' on true or false. on click, DOM updates immediatly : if checked, object appears and if not, disappears. on submit, object is persisted in database.
<form #submit.prevent="submit()">
<div v-for="(checker_website, index) in website.checkers" :key="index">
<input type="checkbox" class="mr-2" :id="index" :true-value="1" :false-value="0" v-model="website.checkers[index].is_active">
<label :for="index">{{checker_website.checker.name}}</label>
</div>
<div class="text-right mt-5">
<button class="save flex font-semibold py-2 px-4 rounded">Enregistrer</button>
</div>
</form>
submit() {
axios.post('/app/store',{websites: this.websites})
},
also, I have a {{total}} prop which is updated after any change on checkbox. it is calculated this way :
computed: {
total() {
let total = 0;
this.websites.forEach(website => {
website.checkers.forEach(checker => {
if (checker.is_active === 1 && checker.checker.status === 1) {
total += checker.checker.price
}
})
})
return total
}
},
and displayed like this :
<span class="p-6">
Total mensuel : {{total}}€/HT
</span>
what I want is to update DOM right after the submit and I find no way to do that. any help will be very appreciated !
One solution is to convert the total computed prop into a data property that gets updated on submit:
export default {
data() {
return {
total: 0,
}
},
computed: {
// total() {⋯} // now a data property
},
methods: {
submit() {
this.total = this.getTotal()
},
getTotal() {
let total = 0
this.websites.forEach(website => {
website.checkers.forEach(checker => {
if (checker.is_active === 1 && checker.checker.status === 1) {
total += checker.checker.price
}
})
})
return total
},
},
}
demo
Im trying to make an pagination with an select field but i dont get it how the v-model and the value properties should be given, for now i have like this ↓...
This is the select/pagination part in that component..
<p v-on:click="back" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-l cursor-pointer">Zurück</p>
<p class="bg-gray-300 text-gray-800 font-bold py-2 px-4 "><span>{{siteCount}}/{{siteTotal}}</span></p>
<select :value="modelValue" v-on:change="selectPage">
<option v-for="select in select" :value="select">{{select}}</option>
</select>
<p v-on:click="next" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-r cursor-pointer">Weiter</p>
</div>
</template>
<script>
import SearchIcon from "../Assets/SearchIcon";
import NavBar from "./NavBar";
export default {
name: "PaginateSeason",
props: {
data: {
name: String,
description: String,
thumbnail: String,
},
siteCount: Number,
siteTotal: Number,
select: Array,
modelValue: String,
},
components: {
NavBar,
SearchIcon
},
emits: ['next', 'back', 'selectPage'],
methods:{
next() {
this.$emit('next')
},
back() {
this.$emit('back')
},
selectPage() {
this.$emit('selectPage')
}
},
}
</script>
This is the main Component where i fetch anything i need...
<template>
<router-view>
<NavBar :is-disabled="true"></NavBar>
<div v-if="isLoading" class="flex flex-col items-center justify-center mt-9">
<LoaderSpinner></LoaderSpinner>
<p>Daten werden geladen...</p>
</div>
<div v-else>
<PaginateSeason
v-model="count"
:data="seasons"
:site-count="current"
:site-total="siteCountTotal"
:select="select"
#next="paginateNext"
#back="paginateBack"
#selectPage="allCount"
/>
</div>
</router-view>
</template>
<script>
import NavBar from './NavBar'
import PaginateSeason from './PaginateSeason'
import LoaderSpinner from "../Assets/LoaderSpinner";
export default {
name: "AllSeasons",
components: {
PaginateSeason,
NavBar,
LoaderSpinner
},
data(){
return {
seasons: null,
current: 1,
pageSize: 4,
isLoading: true,
siteCountTotal: null,
select: [],
count: '',
}
},
mounted(){
this.list(this.current)
const list = async () => {
await axios.get(`/api/seasons/all`).then(response => {
let meta = response.data.meta
this.siteCountTotal = meta.last_page
for (let i = 1; i <= this.siteCountTotal; i++) {
this.select.push(i)
}
}).catch(error => {
console.log(error)
})
}
list()
},
methods: {
async list(page) {
await axios.get(`/api/seasons/all?page=${page}`).then(response => {
this.seasons = response.data.data
}).catch(error => {
console.log(error)
}).finally(() => {
setTimeout(() => {
this.isLoading = false
},1000)
})
},
paginateNext () {
if (this.seasons.length < this.pageSize) {
this.current = 1
this.list(this.current)
} else {
this.current += 1
this.list(this.current)
}
},
paginateBack () {
if (this.current === 1) {
this.current -= 0
} else {
this.current -= 1
this.list(this.current)
}
},
allCount() {
console.log(this.count)
}
},
IF you can help me with that i would be very gratefull!
The count variable on the AllSeasons component will not be updated, because you pass it as v-model to the PaginateSeason component, but PaginateSeason never emits update:modelValue.
Implement selectPage like this to address this issue:
selectPage(event) {
this.$emit('update:modelValue', event.target.value)
}
You can also keep the selectPage event, or add a wach for the count variable in AllSeasons component to watch for page changes. You may also want to emit the update:modelValue event from next and back functions in the PaginateSeason component.
I'm create a an component which represents my money field.
My target is on add element in list, set zero on money field to add next element in list...
But, my problem is that not working when send using $emit event to clear input to improve usability.
$emit works as described on image bellow
My money field:
<template>
<div class="input-group" #clear="clearInputField()">
<span>{{ title }}</span>
<input ref="displayMoney" type="text" v-model="displayMoney" #focus="isActive = true" #blur="isActive = false" />
</div>
</template>
<script>
export default {
props: {
title: String,
},
data() {
return {
money: 0,
isActive: false,
};
},
methods: {
clearInputField() {
console.log("Its work event");
this.money = 0;
this.displayMoney = "";
},
},
computed: {
displayMoney: {
get: function () {
if (this.isActive) {
return this.money;
} else {
return this.money.toLocaleString("pt-br", { style: "currency", currency: "BRL" });
}
},
set: function (modifiedMoney) {
let newMoney = parseFloat(modifiedMoney.replace(/[^\d.]/g, "."));
if (isNaN(newMoney) || newMoney.length == 0) {
newMoney = 0;
}
this.$emit("input", newMoney);
return (this.money = parseFloat(newMoney));
},
},
},
};
</script>
My principal component
<template>
<div class="wish-list">
<div class="row">
<div class="input-group">
<span>Digite sua meta: </span>
<input ref="descriptionWish" type="text" v-model="descriptionWish" />
</div>
<MoneyField title="Valor (R$): " v-model="valueWish" #keyup.native.enter="addWish" />
<button id="btnCalculate" #click="addWish()">Adicionar a lista de desejos</button>
</div>
<div class="list-items">
<ul>
<li v-for="wish in wishes" :key="wish">{{ wish }}</li>
</ul>
</div>
</div>
</template>
<script>
import MoneyField from "./Fields/MoneyField";
export default {
components: {
MoneyField,
},
data() {
return {
wishes: [],
valueWish: 0,
descriptionWish: "",
};
},
methods: {
addWish() {
if (!isNaN(this.valueWish) && this.valueWish > 0 && this.descriptionWish.length > 0) {
this.wishes.push(
`${this.descriptionWish} => ${this.valueWish.toLocaleString("pt-BR", { currency: "BRl", style: "currency" })}`
);
this.descriptionWish = "";
console.log("addWish");
this.valueWish = 0;
this.$emit("clear");
this.$refs.descriptionWish.focus();
}
this.valueWish = 0;
},
},
};
</script>
I still don't understand much about vueJS, but I believe it's something related to parent and child elements, but I've done numerous and I can't get my answer.
sorry for my bad english .
The emit sends an event from the child to the parent component not as you've done, to run a method from the child component you could add a ref in the component inside the parent one like :
<MoneyField title="Valor (R$): "
ref="moneyField" v-model="valueWish" #keyup.native.enter="addWish" />
then run this.$refs.moneyField.clearInputField() instead this.$emit("clear")
I'm very new to Vuejs, I'm following their documentation which is very helpful. However, I find it difficult to understand how compute properties actually are triggered.
I'm using ag-grid for my project and I would like to update the total number of rows to my custom page size drop-down list.
The following is my code:
<template>
<div id="ag-grid-demo">
<vx-card>
<!-- TABLE ACTION ROW -->
<div class="flex flex-wrap justify-between items-center">
<!-- ITEMS PER PAGE -->
<div class="mb-4 md:mb-0 mr-4 ag-grid-table-actions-left"></div>
<!-- TABLE ACTION COL-2: SEARCH & EXPORT AS CSV -->
<div class="flex flex-wrap items-center justify-between ag-grid-table-actions-right">
<vs-button class="mb-4 md:mb-0" #click="gridApi.exportDataAsCsv()">Export as CSV</vs-button>
</div>
</div>
<ag-grid-vue
ref="agGridTable"
:gridOptions="gridOptions"
class="ag-theme-material w-100 my-4 ag-grid-table"
:columnDefs="columnDefs"
:defaultColDef="defaultColDef"
:rowModelType="rowModelType"
#grid-ready="onGridReady"
rowSelection="multiple"
colResizeDefault="shift"
:animateRows="true"
:pagination="true"
:paginationPageSize="paginationPageSize"
:cacheBlockSize="cacheBlockSize"
:enableRtl="$vs.rtl"
:modules="modules"
></ag-grid-vue>
<div class="flex flex-wrap justify-between items-center">
<!-- CUSTOM PAGESIZE DROP-DWON -->
<div class="mb-4 md:mb-0 mr-4 ag-grid-table-actions-left">
<vs-dropdown vs-trigger-click class="cursor-pointer">
<div class="p-4 border border-solid d-theme-border-grey-light rounded-full d-theme-dark-bg cursor-pointer flex items-center justify-between font-medium">
<span class="mr-2"
>{{ currentPage * paginationPageSize - (paginationPageSize - 1) }} - {{ recordCount - currentPage * paginationPageSize > 0 ? currentPage * paginationPageSize : recordCount }} of {{ recordCount }}</span>
<feather-icon icon="ChevronDownIcon" svgClasses="h-4 w-4" />
</div>
<vs-dropdown-menu>
<vs-dropdown-item #click="gridApi.paginationSetPageSize(10)">
<span>10</span>
</vs-dropdown-item>
<vs-dropdown-item #click="gridApi.paginationSetPageSize(50)">
<span>50</span>
</vs-dropdown-item>
<vs-dropdown-item #click="gridApi.paginationSetPageSize(100)">
<span>100</span>
</vs-dropdown-item>
<vs-dropdown-item #click="gridApi.paginationSetPageSize(150)">
<span>150</span>
</vs-dropdown-item>
</vs-dropdown-menu>
</vs-dropdown>
</div>
<!-- CUSTOM TABLE PAGINATION -->
<div class="flex flex-wrap items-center justify-between ag-grid-table-actions-right">
<vs-pagination :total="totalPages" :max="maxPageNumbers" v-model="currentPage" />
</div>
</div>
</vx-card>
</div>
</template>
<script>
import { AgGridVue } from "ag-grid-vue";
import { ServerSideRowModelModule } from "#ag-grid-enterprise/server-side-row-model";
import { MenuModule } from "#ag-grid-enterprise/menu";
import { ColumnsToolPanelModule } from "#ag-grid-enterprise/column-tool-panel";
import CompanyServices from "../../../_services/company.service";
import "#/assets/scss/vuexy/extraComponents/agGridStyleOverride.scss";
export default {
components: {
AgGridVue
},
data() {
return {
gridOptions: {},
maxPageNumbers: 7,
gridApi: null,
defaultColDef: {
sortable: true,
editable: false,
resizable: true,
suppressMenu: false
},
columnDefs: [
{ headerName: "Id", field: "id", filter: false },
{
headerName: "Company Name",
field: "companyName",
filter: true,
checkboxSelection: true,
headerCheckboxSelectionFilteredOnly: true
}
],
rowModelType: "serverSide",
modules: [ServerSideRowModelModule, MenuModule, ColumnsToolPanelModule],
cacheBlockSize: 10,
};
},
computed: {
paginationPageSize() {
if (this.gridApi) return this.gridApi.paginationGetPageSize();
else return 10;
},
totalPages() {
if (this.gridApi) return this.gridApi.paginationGetTotalPages();
else return 0;
},
currentPage: {
get() {
if (this.gridApi) return this.gridApi.paginationGetCurrentPage() + 1;
else return 1;
},
set(val) {
this.gridApi.paginationGoToPage(val - 1);
}
},
recordCount: function() {
if (window.recordCount === undefined) return 0;
else return window.recordCount;
}
},
methods: {
onGridReady: function(params) {
var datasource = new ServerSideDatasource();
params.api.setServerSideDatasource(datasource);
}
},
mounted() {
this.gridApi = this.gridOptions.api;
this.gridColumnApi = this.gridOptions.columnApi;
}
};
window.ServerSideDatasource = function ServerSideDatasource(server) {
return {
getRows: function(params) {
CompanyServices.list({
startRow: params.request.startRow,
endRow: params.request.endRow,
SortColumn: "CompanyName",
SortOrder: "asc"
})
.then(response => {
window.recordCount = response.data.total;
params.successCallback(response.data.rows, response.data.total);
})
.catch(error => {
params.failCallback();
});
}
};
};
</script>
My issue is that computed property 'recordCount' does not get updated as 'window.recordCount' being changed. 'recordCount' always shows value as zero.
Could someone please shed a light here?
Thanks in advance!
There seems to be a couple of issues here, mainly that you are setting data and functions outside of the vue instance.
First of all, you should not be setting any data on window, Vue has plenty of ways to handle your data and makes it all reactive for you.
Start by moving your window.ServerSideDataSource function into the vue instance.
In Vue, functions are put under "methods", so after your onGridReady function, add a ,
and then put:
serverSideDatasource() {
return {
getRows: functions(params) {..}
}
}
Notice that I put serverSideDatasource with a small starting s, as camelCase is widely considered best practice when naming variables and functions.
Next, your window.recordCount should be put into Vue's data. Just add it after cacheBlockSize and set it like so:
recordCount: 0
Now that all your data and methods (functions) are within Vue, you can reach them by using "this".
So replace "window." with "this." all the places you use it.
Now, since your recordCount starts as 0, and is only changed when you call getRows, there is no need for a computed called recordCount, so delete that.
Notice: Having both a property in data and computed called recordCount would conflict, so even if you kept it, you would have to rename the computed.
Do tell me if this fixes it, or how far it gets you :)
I have a User-Select component that wraps a v-select. This components job is to fetch list of users as user types and allow the user to select one or more users.
Here is the code for this component.
<template>
<div>
<v-select
id="recipients"
name="recipients"
:options="options"
label="name"
multiple
#search="onSearch"
v-model="selectedVal"
/>
</div>
</template>
<script>
import vSelect from 'vue-select'
import axios from 'axios'
import _ from 'lodash'
export default {
name: 'UserSelect',
components: {
'v-select': vSelect
},
props: {
value: {
type: Array
}
},
data() {
return {
options: []
}
},
computed: {
selectedVal: {
get() {
return this.value
},
set(val) {
//this.value = val
this.$emit('input', val)
}
}
},
methods: {
onSearch(search, loading) {
loading(true)
this.search(loading, search, this)
},
setSelected: function(val) {
this.$emit('input', val)
},
search: _.debounce((loading, search, vm) => {
axios
.post('searchPeople', { search }, { useCredentails: true })
.then(res => {
vm.options = res.data
loading(false)
})
}, 350)
}
}
</script>
<style lang="scss" scoped>
#import url('https://unpkg.com/vue-select#latest/dist/vue-select.css');
</style>
As you can see I have a v-model linked to a computed property , which emits input event. Also my property name is value. Hence, I expect the parent component,that is using this UserEvent component to be able to v-model.
In the parent component , i have a computed property to which I have v-modelled the selected value. Here is the code.
<template>
<div>
<b-modal id="editMessage" :title="title" :static="true">
<form id="newMessageForm" class="row">
<div class="col-md-12">
<div class="form-group row">
<label for="to" class="col-sm-3 col-form-label">To:</label>
<user-select
class="col-sm-7"
style="padding-left: 0px; padding-right: 0px"
v-model="editedMessage.recipients"
/>
</div>
<div class="form-group row">
<label for="subject" class="col-sm-3 col-form-label"
>Subject:</label
>
<input
id="subject"
name="subject"
type="text"
class="form-control col-sm-7"
:value="editedMessage.messageSubject"
/>
</div>
<div class="form-group row">
<label for="date" class="col-sm-3 col-form-label"
>Schedule for later :
</label>
<input
type="checkbox"
class="form-control col-sm-7"
v-model="scheduleForLater"
id="scheduleCheckBox"
/>
</div>
<div class="form-group row" v-if="scheduleForLater">
<label for="date" class="col-sm-3 col-form-label"
>Scheduled Date:</label
>
<datetime
v-model="editedMessage.sentDate"
type="datetime"
class="col-sm-17"
input-class="form-control col-sm-15"
input-id="date"
/>
</div>
<div class="form-group row">
<label for="body" class="col-sm-3 col-form-label">Message:</label>
<textarea
id="body"
name="body"
type="text"
rows="10"
class="form-control col-sm-7"
:value="editedMessage.messageBody"
></textarea>
</div>
</div>
</form>
<template v-slot:modal-footer="{ hide }">
<!-- Emulate built in modal footer ok and cancel button actions -->
<b-button size="sm" variant="light" #click="hide()">
Cancel
</b-button>
<b-button
size="sm"
variant="secondary"
#click="
sendMessage(true)
hide()
"
>
Save Draft
</b-button>
<b-button
size="sm"
variant="primary"
#click="
sendMessage(false)
hide()
"
>
Send
</b-button>
</template>
</b-modal>
</div>
</template>
<script>
import { mapState } from 'vuex'
import UserSelect from '#/components/UserSelect.vue'
import axios from 'axios'
export default {
name: 'NewMessage',
components: {
'user-select': UserSelect
},
data() {
return {
options: [],
scheduleForLater: false
}
},
mounted() {},
computed: {
...mapState({
openMessage: state => state.message.openMessage,
messageAction: state => state.message.messageAction
}),
editedMessage: {
get() {
if (this.messageAction === 'new') {
return this.newMessage()
} else if (this.messageAction === 'reply') {
let openMessageClone = Object.assign({}, this.openMessage)
// make sender as the recipient.
return Object.assign(openMessageClone, {
messageSubject: 'RE: ' + this.openMessage.messageSubject,
recipients: [
{
name: this.openMessage.sender.name,
id: this.openMessage.sender.id
}
]
})
} else {
let openMessageClone = Object.assign({}, this.openMessage)
return Object.assign(openMessageClone, {
messageSubject: 'FW: ' + this.openMessage.messageSubject
})
}
},
set(val) {
this.$emit('input', val)
}
},
title() {
if (this.messageAction === 'new') {
return 'New'
} else if (this.messageAction === 'reply') {
return 'Reply'
} else {
return 'Forward'
}
}
},
methods: {
newMessage() {
return {
messageBody: '',
messageSubject: '',
recipients: [],
sender: {}
}
},
sendMessage(saveOrUpdateDraft) {
var url = ''
var message = {
recipients: this.editedMessage.recipients.map(x => x.id),
subject: this.editedMessage.messageSubject,
body: this.editedMessage.messageBody,
sentDate: this.scheduleForLater ? this.editedMessage.sentDate : null,
id: ['editDraft', 'viewScheduled'].includes(this.messageAction)
? this.editedMessage.messageId
: null
}
// id indiciates message id of draft message if one was opened.
if (saveOrUpdateDraft) {
// if no changes have been made to an opened draft, or the draft is completely empty for a new or existing draft , just go back.
if (message.id) {
url = `updateDraft`
} else {
url = 'saveNewDraft'
}
} else {
if (message.id) {
url = `sendSavedMessage`
} else {
url = 'sendMessage'
}
}
axios
.post(`eventdirect/${url}`, message, {
withCredentials: true
})
.then(response => {
if (url.includes('Draft') && this.messageAction === 'viewScheduled') {
this.$store.dispatch('sent/moveToDraft', response.data)
} else if (url.includes('Draft')) {
this.$store.dispatch('drafts/updateDraft', response.data)
} else {
// else part is sending a message or scheduling a draft.
if (this.messageAction === 'editDraft') {
this.$store.dispatch('drafts/deleteDraft', response.data)
}
// if we are sending an existing scheduled messsage , just update the sent vuex store , so that the message moves from scheduled to sent bucket.
if (this.messageAction === 'viewScheduled') {
this.$store.dispatch('sent/updateMessage', response.data)
} else {
this.$store.dispatch('sent/addSentItem', response.data)
}
}
})
.catch(() => {
// TODO , add a qtip here to notify user , this message should be sent later.
// messages in draft store with target , should be posted to the target
this.$store.dispatch('drafts/updateDraft', {
...message,
target: url
})
})
}
}
}
</script>
Now i can see in the vue dev tools the computed values in this NewMessage component gets changed. However this component does not re-render and the selected values are not passed down to UserSelect component until I toggle , schedule for later checkbox , that causes the components data to change and this triggers the Vue component to suddenly start showing the selected values.
Whats going on here. There is something about Vue's reactivity that I am not able to understand here.
Thanks in anticipation. You can try it here, or click on the edit sandbox button above to view and edit the code.
To try it, click on the Vue Message link , then hit reply , then type ‘Test’ and then select ‘Test User’ from the drop down. The selected user wont show until you click the checkbox - Schedule later.
PS: Within the component , UserSelect, In the setter of the computed property selectedVal , if I manually set the value of property value (by simply uncommenting line 39 - which is commented right now) , everything works fine. However , I get a warning that property should not be directly set. Since I am emitting the input event , parent component should change its v-model and re-render , thereby causing child component to re-render. Please correct me , if my understanding is wrong. Problem once again is that the parent component’s v-model changes , but it doesn’t re-render.