Error when attempting to disable Navigation Buttons in VueJs - vue.js

In my ticket processing application I currently have a back and forward button contained in my TicketRunner.vue Component, I would like to change it so that these buttons only appear if I have an associated case file, for which I've used V-If:
TicketRunner.Vue
<div class="level nav-btns" v-if='!currentTicketCaseFiles.length'>
<div class="buttons has-addons level-left">
<b-button
#click.prevent="navGoPrev()"
:disabled="currentStepIndex === 0 || navWaiting"
size="is-medium"
>
</div>
export default {
name: 'TicketRunner',
mixins: [NavStepsByIndexMixin()],
components: {
StagePresenter,
CaseFilesStage,
ParticipantsStage,
AttachmentsStage,
CaseFilesRunner,
TicketContextButtons,
},
data: function() {
return {
firstComponentsInitialization: true,
loadingConfirm: false,
confirmationModalActive: false,
confirmationSucceeded: undefined
}
},
props: {
ticketId: {
type: Number,
required: true,
},
},
provide() {
return {
contextButtons: {
capture: (name, callback, title) => this.$refs['contextButtons'].captureButton(name, callback, title),
release: (name) => this.$refs['contextButtons'].releaseButton(name),
enable: (name) => this.$refs['contextButtons'].enableButton(name),
disable: (name) => this.$refs['contextButtons'].disableButton(name),
},
};
},
computed: {
...mapGetters(['currentTicket', 'ticketCaseFiles', 'allCurrentTicketAttachments', 'currentTicketCaseFileNotAssociated',
'currentRequesterType', 'currentTicketStage', 'lastCaseFile']),
caseFiles() {
return this.ticketCaseFiles(this.ticketId);
},
ticketHasAttachments() {
return this.allCurrentTicketAttachments.length > 0;
},
isTicketAssociatedWithCaseFile() {
return !this.currentTicketCaseFileNotAssociated;
},
isFirstNavInitializationInProgress() {
return !this.navReady && this.firstComponentsInitialization;
},
isShowAttachmentsStep() {
return this.ticketHasAttachments && this.currentRequesterType !== 'unknown' &&
(this.isFirstNavInitializationInProgress || this.isTicketAssociatedWithCaseFile)
},
isCurrentTicketResolved() {
return this.currentTicket.status === 'resolved';
},
islastStep() {
return this.navLastStep() && this.lastCaseFile;
}
},
watch: {
ticketId(){
this.navigator.reset();
},
navReady() {
this.moveForwardIfReady();
this.firstComponentsInitialization = false;
}
},
methods: {
...mapActions(['confirmTicket']),
moveForwardIfReady() {
if (this.navigator.currentIndex === 0 && this.firstComponentsInitialization) {
let steps = 0
const step_names = ['case_files_stage']
for(const [_idx, name] of step_names.entries()) {
const ref_name = `step[${name}]`;
if (this.$refs.hasOwnProperty(ref_name) && this.$refs[ref_name].navReady) {
steps += 1
} else {
break
}
}
this.navigator.currentIndex += steps
}
},
confirm() {
this.$buefy.dialog.confirm({
message: this.t('tickets.stages.confirmation.simplified_confirm_reply'),
onConfirm: () => this.confirmStep()
})
},
async confirmStep() {
this.loadingConfirm = true;
const promise = this.confirmTicket(this.ticketId);
return promise.then((response) => {
this.confirmationModalActive = true;
this.confirmationSucceeded = true;
return true; // true is correct here. for goNext it makes parent to stay on on the current step
}).catch(() => {
this.confirmationModalActive = true;
this.confirmationSucceeded = false;
return true; // true is correct here. for goNext it makes parent to stay on on the current step
}).finally(() => this.loadingConfirm = false);
},
},
};
I then receive the following Console Error:
[Vue warn]: Property or method "currentTicketCaseFiles" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
I know that "!currentTicketCaseFiles.length" works successfully in the Component CaseFilesStage.vue, which makes me believe I should somehow connect the two? But importing it doesn't seem right to me either. I'm not quite sure how to tackle this issue as I'm quite new at VueJS, and would be happy for any pointers. I'll attach the CaseFilesStage.vue Component below.
CaseFilesStage.vue
<template>
<div class="hero">
<div class="block">
<template v-if="!currentTicket.spamTicket">
<b-field>
<b-input
v-model="filter"
:loading="loading"
:placeholder="t('tickets.stages.case_files.search.tooltip')"
v-on:keyup.enter.native="searchCaseFiles"
type="search"
icon="search"
:class="{ 'preview-enabled': showAttachmentsPreview}"
/>
</b-field>
<template v-if="foundCaseFiles.length">
<h4 class="title is-4 table-title">{{ t('tickets.stages.case_files.search.table_title') }}</h4>
<CaseFilesSearchTable
:case-files="foundCaseFilxes"
:found-by-data-points="foundCaseFilesByParticipant"
:show-header="true"
v-slot="cf">
<b-checkbox v-if="cfBelongsToCurrentTicket(cf.id)" :disabled="true" :value="true"></b-checkbox>
<b-checkbox v-else #input="onFoundCaseFile(cf.id, $event)"></b-checkbox>
</CaseFilesSearchTable>
</template>
<div v-else-if="lookupStatus === 'notFound'">
{{ t('tickets.stages.case_files.search.not_found') }}
<!-- display button here if above is activated -->
</div>
</template>
</div>
<template v-if='currentTicketCaseFiles.length'>
<h4 class="title is-4 table-title">{{ t('tickets.stages.case_files.table_title') }}</h4>
<CaseFilesTable :case-files="currentTicketCaseFiles" :show-header="true" v-slot="cf">
<DeleteButton
:model-id="cf.id"
modelName="CaseFile" >
</DeleteButton>
</CaseFilesTable>
</template>
</div>
</template>
<script>
import CaseFilesTable from '../tables/CaseFilesTable';
import CaseFilesSearchTable from '../tables/CaseFilesSearchTable';
import DeleteButton from '../../../../shared/components/controls/DeleteButton';
import { mapGetters, mapActions } from 'vuex';
import { mapServerActions } from "../../../../../../_frontend_infrastructure/javascript/lib/crudvuex_new";
export default {
name: 'CaseFilesStage',
data() {
return {
lookupStatus: 'waitingInput',
filter: '',
waiting: {},
foundCaseFiles: [],
foundCaseFilesByParticipant: {}
};
},
components: {
CaseFilesTable,
CaseFilesSearchTable,
DeleteButton
},
computed: {
...mapGetters(
['currentTicketCaseFiles', 'currentTicketCaseFileNotAssociated', 'currentTicket', 'showAttachmentsPreview']
),
loading() {
return this.lookupStatus === 'waitingServer';
},
allCaseFilesMix(){
this.currentTicketCaseFiles + this.foundCaseFiles
},
foundCaseFilesEx() {
return this.foundCaseFiles.filter((x) => !this.cfBelongsToCurrentTicket(x.id))
},
checkboxValue() {
if(!this.currentTicketCaseFileNotAssociated) {
return null;
}
return true;
},
navReady() {
return this.currentTicket.spamTicket || this.currentTicketCaseFiles.length > 0 || this.checkboxValue;
},
markSpam: {
get: function() {
return this.currentTicket.spamTicket
},
set: function(val) {
return this.updateTicket([this.currentTicket.id, { spam_ticket: val }]);
},
}
},
methods: {
...mapActions(['updateTicket']),
...mapServerActions(['createCaseFile', 'deleteCaseFile']),
cfBelongsToCurrentTicket(id){
return this.currentTicketCaseFiles.map((x) => x.caseFileId).includes(id);
},
cantAssignCaseFileCheckbox(isChecked){
if(isChecked) {
this.createCaseFile({ isCfNotAssociated: true });
} else {
this.deleteCaseFile(this.currentTicketCaseFileNotAssociated);
}
},
onFoundCaseFile(id, useIt){
console.log("onFoundCaseFile: ", id, useIt);
if(useIt) {
this.createCaseFile({ caseFileId: id });
} else {
this.deleteCaseFile(this.currentTicketCaseFiles.find({ caseFileId: id }));
}
},
searchCaseFiles() {
const newData = this.filter;
if (newData.length < 3) { // TODO: some smarter condition here
this.foundCaseFiles = [];
this.lookupStatus = 'waitingInput';
return;
}
this.lookupStatus = 'waitingServer';
this.$axios.get('case_files', { params: { "case_files.filter" : newData } })
.then((response) => {
this.foundCaseFiles = response.data.caseFilesSearchResult.caseFiles;
this.foundCaseFilesByParticipant = response.data.caseFilesSearchResult.foundByPrivateInfo;
if(this.foundCaseFiles.length > 0) {
this.lookupStatus = 'success';
} else {
this.lookupStatus = 'notFound';
}
}).catch(() => this.lookupStatus = 'error');
}
},
};
</script>
</style>

Add this to your TicketRunner.vue Component script:
computed: {
...mapGetters(['currentTicketCaseFiles'])
}

Related

Parsing error: Missing semicolon in Vue.js

I am trying to implement a rich text editor in Vue.js for a learning purpose. I've been following approach here:
https://roe.dev/blog/building-your-own-vue-rich-text-component
But I'm receiving an error while loading my localhost page.
Is my code missing something? Here are crucial parts:
App.vue ->
<template>
<div><Richinput/></div>
</template>
<script>
import Richinput from './components/Richinput.vue'
export default {
name: 'App',
components: {
Richinput
}
}
</script>
RichInput.vue ->
<template>
<div contenteditable #input="handleInput" #keydown="handleKeydown" />
</template>
<script lang="ts">
const exec = (command: string, value?: string) =>
document.execCommand(command, false, value);
const queryCommandValue = (command: string) =>
document.queryCommandValue(command);
export default {
props: {
value: { type: String, default: "" },
},
mounted() {
this.$el.innerHTML = this.value;
},
watch: {
value(newValue) {
if (this.$el.innerHTML !== newValue) this.$el.innerHTML = newValue;
},
},
methods: {
handleInput(e: InputEvent | KeyboardEvent) {
const { firstChild } = e.target as HTMLElement;
if (firstChild && firstChild.nodeType === 3) exec("formatBlock", "<p>");
else if (this.$el.innerHTML === "<br>") this.$el.innerHTML = "";
this.$emit("input", this.$el.innerHTML);
},
handleDelayedInput(e: KeyboardEvent) {
this.$nextTick(() => this.handleInput(e));
},
},
handleKeydown(e: KeyboardEvent) {
if (
e.key.toLowerCase() === "enter" &&
queryCommandValue("formatBlock") === "blockquote"
) {
this.$nextTick(() => exec("formatBlock", "<p>"));
} else if (e.ctrlKey) {
switch (e.key.toLowerCase()) {
case "b":
e.preventDefault();
this.$nextTick(() => exec("bold"));
break;
case "i":
e.preventDefault();
this.$nextTick(() => exec("italic"));
break;
case "u":
e.preventDefault();
this.$nextTick(() => exec("underline"));
break;
default:
break;
}
}
},
};
</script>

How to set default settings for switch turned on enabled with VueJS?

I have a script coded in VueJS. It currently has two switch buttons that are called R-18 & R18G and they both currently unchecked on default. The function of these switches do is it will filter content that is displayed on the site if you toggle the switch to checked. What I want to do is have both switches to be set checked on default. How can I fix this?
<template>
<div class="setting">
<van-cell center title="cache" :label="size.local | bytes">
<template #right-icon>
<van-button type="primary" size="small" #click="clearCache('local')">clean up</van-button>
</template>
</van-cell>
<van-cell center title="runtime cleanup" :label="size.session | bytes">
<template #right-icon>
<van-button type="info" size="small" #click="clearCache('session')">clean up</van-button>
</template>
</van-cell>
<van-cell center title="R-18" label="NSFW">
<template #right-icon>
<van-switch :value="currentSETTING.r18" #input="onR18Change($event, 1)" size="24" />
</template>
</van-cell>
<van-cell center title="R-18G" label="Extreme NSFW">
<template #right-icon>
<van-switch :value="currentSETTING.r18g" #input="onR18Change($event, 2)" size="24" />
</template>
</van-cell>
</div>
</template>
<script>
import { Cell, Switch, Button, Dialog } from "vant";
import { mapState, mapActions } from "vuex";
import { LocalStorage, SessionStorage } from "#/utils/storage";
export default {
name: "Setting",
data() {
return {
currentSETTING: {
r18: false,
r18g: false
},
size: {
local: 0,
session: 0
}
};
},
computed: {
...mapState(["SETTING"])
},
watch: {
$route() {
this.calcCacheSize();
}
},
methods: {
onR18Change(checked, type) {
let name;
if (type === 1) name = "R-18";
if (type === 2) name = "R-18G";
if (checked) {
Dialog.confirm({
message: `Are you sure you want to enable the display of R-18`,
confirmButtonColor: "black",
cancelButtonColor: "#1989fa",
closeOnPopstate: true
})
.then(() => {
if (type === 1) this.currentSETTING.r18 = checked;
if (type === 2) {
this.currentSETTING.r18g = checked;
setTimeout(() => {
Dialog.alert({
message: `Please note that turning on R-18G switch may have irreversible effects `
});
}, 200);
}
})
.catch(() => {
console.log("cancel");
});
} else {
if (type === 1) this.currentSETTING.r18 = checked;
if (type === 2) this.currentSETTING.r18g = checked;
}
},
calcCacheSize() {
this.size.local = LocalStorage.size;
this.size.session = SessionStorage.size;
},
clearCache(type) {
let showName;
switch (type) {
case "local":
showName = "cache";
break;
case "session":
showName = "runtime cache";
break;
default:
break;
}
Dialog.confirm({
message: `Are you sure you want to clean up?`,
confirmButtonColor: "black",
cancelButtonColor: "#1989fa",
closeOnPopstate: true
}).then(() => {
if (type === "local") LocalStorage.clear();
if (type === "session") SessionStorage.clear();
this.calcCacheSize();
this.$toast.success("cleaned up");
});
},
...mapActions(["saveSETTING"])
},
filters: {
bytes(bytes) {
bytes = Number(bytes);
if (bytes === 0) return "0 B";
const k = 1024;
const dm = 0;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
},
mounted() {
this.currentSETTING = JSON.parse(JSON.stringify(this.SETTING));
this.calcCacheSize();
},
updated() {
this.saveSETTING(JSON.parse(JSON.stringify(this.currentSETTING)));
},
components: {
[Cell.name]: Cell,
[Button.name]: Button,
[Switch.name]: Switch
}
};
</script>
<style lang="stylus" scoped>
.setting {
}
</style>
The comment of Excalibaard is correct.
When you declare your data in the vue component, whatever value you pass to them, that will be the inital state for them.
In your case if you data values to true will work for you.
Be careful, that when we request and populate our component's data, we declare them as null. That will prevent any unwanted initial app states, and ensure that you will only have access on them after request response.

Don't send Axios request when user selects a result

I'm using v-autocomplete from vuetify.js to retrieve a list of values from API Server.
It works fine and my list of values is not empty.
But my problem is when I select the correct value from this list. My script sends another request to server to retrieve another autocomplete list.
Do you have any idea to avoid to send request when a result is selected by the user ? Or to send request only when a key is down ?
My component :
<template>
<div>
<v-autocomplete
v-model="selectValeur"
:loading="loading"
:search-input.sync="search"
:items="resultatsAutocomplete"
class="mb-4"
hide-no-data
hide-details
:label="recherche.label"
></v-autocomplete>
</div>
</template>
<script>
export default {
props: {
recherche: {
type: Object,
default: null,
},
},
data: () => ({
selectValeur: null,
loading: false,
search: null,
resultatsAutocomplete: [],
}),
watch: {
selectValeur(oldval, val) {
console.log(oldval)
console.log(val)
},
search(val) {
val && val !== this.selectValeur && this.fetchEntriesDebounced(val)
console.log(val)
if (!val) {
this.resultatsAutocomplete = []
}
},
},
methods: {
fetchEntriesDebounced(val) {
// cancel pending call
clearTimeout(this._timerId)
// delay new call 500ms
this._timerId = setTimeout(() => {
this.querySelections(val)
}, 500)
},
async querySelections(v) {
if (v.length > 1) {
this.loading = true
try {
const result = await this.$axios.$get(
'myapi/myurl',
{
params: {
racine: v,
},
}
)
this.resultatsAutocomplete = result
console.log(this.resultatsAutocomplete)
this.loading = false
} catch (err) {
console.log(err)
this.loading = false
}
} else {
this.resultatsAutocomplete = []
}
},
},
}
</script>
Thanks,
selectValeur would no longer be null if the user has selected a value, so you could update search() to return if selectValeur is truthy:
export default {
watch: {
search(val) {
if (this.selectValeur) {
// value already selected
return
}
//...
}
}
}
Or you could use vm.$watch on the search property to be able to stop the watcher when selectValeur is set:
export default {
mounted() {
this._unwatchSearch = this.$watch('search', val => {
val && val !== this.selectValeur && this.fetchEntriesDebounced(val)
if (!val) {
this.resultatsAutocomplete = []
}
})
},
watch: {
selectValeur(val) {
if (val && this._unwatchSearch) {
this._unwatchSearch()
}
}
}
}
I found a solution to my problem.
I used the #keyup event to send the axios request and I deleted the watcher on search.
So, the API request are only sent when I press a key.
<template>
<div>
<v-autocomplete
v-model="selectValeur"
:loading="loading"
:items="resultatsAutocomplete"
:search-input.sync="search"
class="mb-4"
hide-no-data
hide-details
:label="recherche.label"
#keyup="keyupSearch"
></v-autocomplete>
</div>
</template>
<script>
export default {
props: {
recherche: {
type: Object,
default: null,
},
},
data: () => ({
selectValeur: null,
loading: false,
resultatsAutocomplete: [],
search: '',
}),
methods: {
keyupSearch(val) {
val &&
val !== this.selectValeur &&
this.fetchEntriesDebounced(this.search)
if (!val) {
this.resultatsAutocomplete = []
}
},
fetchEntriesDebounced(val) {
// cancel pending call
clearTimeout(this._timerId)
// delay new call 500ms
this._timerId = setTimeout(() => {
this.querySelections(val)
}, 500)
},
async querySelections(v) {
if (v.length > 1) {
this.loading = true
try {
const result = await this.$axios.$get(
'my-api/my-url',
{
params: {
sid: this.$route.params.sid,
service: this.$route.params.service,
type: this.recherche.mode,
racine: v,
},
}
)
this.resultatsAutocomplete = result
console.log(this.resultatsAutocomplete)
this.loading = false
} catch (err) {
console.log(err)
this.loading = false
}
} else {
this.resultatsAutocomplete = []
}
},
},
}
</script>

Vue-Apollo and query autocomplete

I'm new to Vue and i'm trying now to do a query update on q-autocomplete component (i have to use Quasar Framework).
This is the FormAdmin component:
<template>
<q-form #submit="submit" #reset="onReset" class="q-gutter-md">
<q-select
filled
v-model="form.UserId"
label="User Email"
use-input
hide-selected
fill-input
input-debounce="0"
:options="options"
#filter="filterFn"
hint="Mininum 2 characters to trigger autocomplete"
style="width: 250px; padding-bottom: 32px"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
<div>
<q-btn label="Submit" type="submit" color="primary"/>
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</template>
This is the basic code for the update function into the filterFn:
<script>
export default {
data () {
return {
model: null,
options: null,
}
},
props: ['form', 'submit'],
methods: {
filterFn (val, update, abort) {
if (val.length < 3) {
abort()
return
}
update(() => {
console.log(val);
})
},
onReset(){
},
}
}
</script>
I tried this code:
<script>
import gql from 'graphql-tag';
const stringOptions = gql`
query {
filterUsersListFirst {
UserEmail
}
}`
export default {
data () {
return {
model: null,
options: Object.values(stringOptions),
}
},
props: ['form', 'submit'],
methods: {
filterFn (val, update, abort) {
if (val.length < 3) {
abort()
return
}
this.$apollo.query({
query: gql`query($email: String!) {
filterUsersListByEmail(
email: $email
) {
UserEmail
}
}`,
variables: {
email: val,
}
}).then(data => {
console.log(JSON.stringyfy(data));
this.options = Object.values(data);
}).catch(error =>{
console.log({error});
});
},
onReset(){
},
}
}
</script>
I tried graphql query server-side and it works.
Also i tried to print data that the Promise returns and it results in:
{"data":{"filterUsersListByEmail":[{"UserEmail":"email1","__typename":"User"},{...}]},"loading":false,"networkStatus":7,"stale":false}
But i don't know how to append the query result in the q-select options.
I found myself the solution and i'll write it:
methods: {
filterFn (val, update, abort) {
if (val.length < 3) {
abort()
return
}
setTimeout(() => {
update(() => {
this.$apollo.query({
query: gql`query($email: String!) {
filterUsersListByEmail(
email: $email
) {
UserId
UserEmail
}
}`,
variables: {
email: val,
}
}).then(data => {
var emailList = [];
for(var i = 0; i < data.data.filterUsersListByEmail.length; i++)
{
emailList[i] = JSON.parse('{"label":"' + data.data.filterUsersListByEmail[i].UserEmail + '", "value":"' + data.data.filterUsersListByEmail[i].UserId + '"}');
}
this.options = usersList;
}).catch(error =>{
console.log({error});
});
})
}, 1000)
},
onReset(){
...
},
}

Variable not updated after vuex mutation

I am creating a settings page, where I fetch some data from the API and I am using Vuex to handle mutations.
I can see that the Vuex completes properly, but value for my dailyCount variable doesn't update in frontend.
This is my Settings component:
<template>
<div>
<div class="row col">
<h1>Settings</h1>
</div>
<div class="row col">
<div class="well">
<form class="form-inline">
<input type="number" v-model="dailyCount" />
{{ dailyCount }}
</form>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'settings',
data () {
return {
dailyCount: 500
};
},
created () {
this.$store.dispatch('settings/fetchSetting');
},
computed: {
isLoading() {
return this.$store.getters['user/isLoading'];
},
hasError() {
return this.$store.getters['user/hasError'];
},
error() {
return this.$store.getters['user/error'];
},
},
}
</script>
I do mutations here:
import SettingsAPI from '../api/settings';
export default {
namespaced: true,
state: {
isLoading: false,
error: null,
settings: null,
},
getters: {
isLoading (state) {
return state.isLoading;
},
hasError (state) {
return state.error !== null;
},
error (state) {
return state.error;
},
user (state) {
return state.user;
},
},
mutations: {
['FETCHING_SETTINGS'](state) {
state.isLoading = true;
state.error = null;
state.settings = null;
},
['FETCHING_SETTINGS_SUCCESS'](state, settings) {
state.isLoading = false;
state.error = null;
state.settings = settings;
},
['FETCHING_SETTINGS_ERROR'](state, error) {
state.isLoading = false;
state.error = error;
state.settings = null;
},
},
actions: {
fetchSetting ({commit}) {
commit('FETCHING_SETTINGS');
return SettingsAPI.get()
.then(res => {commit('FETCHING_SETTINGS_SUCCESS', res.data);})
.catch(err => commit('FETCHING_SETTINGS_ERROR', err));
},
},
}
And call to a server is done here (api/settings.js - it is imported in mutation file):
import axios from 'axios';
export default {
get() {
return axios.get('/user');
},
}
Can you see what am I doing wrong? I am trying to debug it using Vuejs debug toolbar, but all seems to work fine.
You need to get store state from vuex and inject to Vue component, either by this.$store.state or this.$store.getters.
For example:
<script>
export default {
name: 'settings',
data () {
return {
dailyCount: 500
};
},
created () {
this.$store.dispatch('settings/fetchSetting');
},
computed: {
isLoading() {
return this.$store.getters['user/isLoading'];
},
hasError() {
return this.$store.getters['user/hasError'];
},
error() {
return this.$store.getters['user/error'];
},
settings() {
return this.$store.state.settings
}
},
watch: {
settings () {
this.dailyCount = this.settings.dailyCount
}
}
}
</script>