I have a form that has a normal save method which validates and saves with validation errors blocking submissions. But, I need a submission that is Save as Draft that bypasses the validation and submits with errors. The normal save and validate is working great, but I can't seem to find a way to easily turn off validation dynamically.
This is the stripped down version of what I have right now:
<template>
<form #submit.prevent="saveForm">
<!-- a bunch of custom components built around useField -->
<button type="submit" #click="item.draft = true">Save as Draft</button>
<button type="submit" #click="item.draft = false">Submit</button>
</form>
</template>
<script setup lang="ts">
import { useForm } from "vee-validate";
import { reactive } from "vue";
const { handleSubmit, isSubmitting, ...formStuff } = useForm();
const item = reactive({
draft: false
});
const saveForm = handleSubmit(async () => {
// do stuff to save here
})
</script>
Being "that guy" who answers their own question is better than "that guy" who just says they solved it I guess...
After lots of digging and some minor refactoring I was able to get this working. The key was changing from field based rules and validation to form based since the validationSchema is reactive. Then I can just change the schema to false to make the form skip validation when it's a draft.
My setup script now looks like the following:
import { useForm } from "vee-validate";
import { computed, reactive } from "vue";
const submissionValidation = {
'consult_patient_name': 'required|min:5',
'item_name': 'required|min:2',
'consult_question': 'required|min:5',
'consult_history': 'required|min:5',
};
// if it's a draft set it to false, so it can bypass validation
// otherwise set it to the actual validation schema
const validationSchema = computed(() =>
item.draft === true
? false
: submissionValidation
);
const { handleSubmit, isSubmitting } = useForm({ validationSchema });
const item = reactive({
draft: false
});
const saveForm = handleSubmit(async () => {
// do stuff to save here
})
Related
I'm just starting with Nuxt and the answer could be obvious, but I'm hoping to get support from you.
I've got a 2 language website, built with Nuxt 3 that uses Nuxt I18n for internationalization, which retrieves data from an API (a strapi headless cms). I've managed to set up a Pinia store in order to not overuse the API, which looks like this:
// /stores/store.js
import { defineStore } from "pinia";
import { useFetch } from "#app";
export const useStore = defineStore("store", {
state: () => ({
data: {
en: [],
ru: []
}
}),
actions: {
async fetchData() {
let resEn = await useFetch('strapi-url.com/api/data', {
params: {
locale: 'en'
}
});
if (resEn.error.value) {
throw createError({
statusCode: resEn.error.value.statusCode,
statusMessage: resEn.error.value.statusMessage
});
}
this.data.en = resEn.data;
let resFr = await useFetch('strapi-url.com/api/data', {
params: {
locale: 'fr'
}
});
if (resFr.error.value) {
throw createError({
statusCode: resFr.error.value.statusCode,
statusMessage: resFr.error.value.statusMessage
});
}
this.data.fr = resFr.data;
}
}
});
And to make the data available when app loads I've setup the app.vue file:
<script setup>
import { useStore } from "~/stores/store";
const store = usetStore();
await store.fetchData();
</script>
<template>
<div>
<Header/>
<NuxtPage/>
<Footer/>
</div>
</template>
and then in a component (ex: Header.vue) I'm getting the data from the store an render it:
<script setup>
import { useStore } from "~/stores/NewsletterStore";
import { storeToRefs } from "pinia";
const { locale } = useI18n();
const store = useStore();
const { data } = storeToRefs(store);
const title = data[locale].title;
</script>
<template>
<div>
{{ title }}
</div>
</template>
The problem is that when the language changes, by a locale switcher, the data isn't refreshed, even if the locale changes too.
I would like to know if there's any way to make it reactive, based on the selected locale.
Thanks & looking forward.
I've tried to setup a pinia store using nuxt 3 web app that has 2 languages controlled by Nuxt I18n module that consumes data from an strapi backend API, but the data rendered isn't reactive when changing locale. I expect to know how to make this data be reactive, when language changes?
<template>
<div class="home">
<h1>BPMN Lint Analyzer</h1>
<!-- Get File from DropZone -->
<DropZone #drop.prevent="drop" #change="selectedFile"/>
<span class="file-info">File:{{dropzoneFile.name}}</span>
<button #click="sendFile" >Upload File</button>
<!-- Display Response Data (Not Working)-->
<div v-if="showResponseData">
<p>Testing: {{responseData}}</p>
</div>
</div>
</template>
<script>
import DropZone from '#/components/DropZone.vue'
import {ref} from "vue"
import axios from 'axios'
export default {
name: 'HomeView',
components: {
DropZone
},
setup(){
let dropzoneFile = ref("")
//Define Response variable and visibility toggle
var responseData=''
// var showResponseData = false
//Methods
const drop = (e) => {
dropzoneFile.value = e.dataTransfer.files[0]
}
const selectedFile = () => {
dropzoneFile.value = document.querySelector('.dropzoneFile').files[0]
}
//API Call
const sendFile = () => {
let formData = new FormData()
formData.append('file', dropzoneFile.value)
axios.post('http://localhost:3000/fileupload', formData,{
headers: {
'Content-Type':'multipart/form-data'
}
}).catch(error => {
console.log(error)
}).then(response => {
responseData = response.data
console.log(responseData);
})
// showResponseData=true
}
return{dropzoneFile, drop, selectedFile, sendFile}
}
}
</script>
I'm trying to pass the response from sendFile, which is stored in responseData back to the template to display it in a div to begin with. I'm not sure if a lifecycle hook is needed.
Current output:
I played around with toggles, I tried to convert everything to options API. Tried adding logs but I'm still struggling to understand what I'm looking for.
Unfortunately I am stuck with the Composition API in this case even if the application itself is very simple. I'm struggling to learn much from the Docs so I'm hoping to find a solution here. Thank you!
You need to make responseData reactive, so try to import ref or reactive from vue:
import {ref} from 'vue'
then create your variable as a reactive:
const responseData = ref(null)
set data to your variable:
responseData.value = response.data
in template check data:
<div v-if="responseData">
<p>Testing: {{responseData}}</p>
</div>
finally return it from setup function (if you want to use it in template):
return{dropzoneFile, drop, selectedFile, sendFile, responseData}
I am currently working on a project and could use some help.
I have a backend with an endpoint which delivers an array of strings with approximately 13k entries. I created a component in DropdownSearch.vue which should be used on several different views with differing inputs. For this specific purpose I used vueform/multiselect. If I only try to add the dropdown without any information it works perfectly. Also if I access the endpoints and console.log() it it will work properly and deliver me an output. But if I try to initialize the output to the dropdown the whole page will stop working, the endpoint won't give me a response and the application freezes.
DropdownSearch.vue
<div>
<Multiselect
class="float-left"
v-model="valueDropdownOne"
mode="tags"
:placeholder="selectorOne"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownOne"
:groups="true"
/>
<Multiselect
class="float-left"
v-model="valueDropdownTwo"
mode="tags"
:placeholder="selectorTwo"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownTwo"
/>
<Multiselect
class="float-left"
v-model="valueDropdownThree"
mode="tags"
:placeholder="selectorThree"
:closeOnSelect="false"
:searchable="true"
:createTag="true"
:options="dropdownThree"
/>
</div>
</template>
<script>
import Multiselect from "#vueform/multiselect";
import { ref }from "vue"
export default {
name: "DropdownSearch",
components: { Multiselect },
props: {
selectorOne: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
selectorTwo: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
selectorThree: {
type: String,
default: "<DEFAULT VALUE>",
required: true,
},
dropdownOne: {
type: Array
}
,
dropdownTwo: {
type: Array
},
dropdownThree: {
type: Array
}
},
setup() {
const valueDropdownOne = ref()
const valueDropdownTwo = ref()
const valueDropdownThree = ref()
return {valueDropdownOne, valueDropdownTwo, valueDropdownThree}
}
};
</script>
<style src="#vueform/multiselect/themes/default.css"></style>
Datenbank.vue
<template>
<div>
<DropdownSearch
selectorOne="Merkmale auswählen"
:dropdownOne="dropdownOne"
selectorTwo="Monographien auswählen"
:dropdownTwo="dropdownTwo"
selectorThree="Orte auswählen"
:dropdownThree="dropdownThree"
></DropdownSearch>
</div>
</template>
<script>
import DropdownSearch from "../components/DropdownSearch.vue";
import { ref, onMounted } from "vue";
export default {
components: { DropdownSearch },
setup() {
const dropdownOne = ref([]);
const dropdownTwo = ref([]);
const dropdownThree = ref([]);
const getPlaces = async () => {
const response = await fetch("http://127.0.0.1:5000/project/get-places");
const places = await response.json();
return places;
};
onMounted(async () => {
const places = await getPlaces();
dropdownThree.value = places;
});
return {
dropdownOne,
dropdownTwo,
dropdownThree
};
},
};
</script>
<style lang="scss" scoped></style>
it is not the problem of vue
the library you used may not support virtual-list, when the amount of data becomes large, the actual dom element will also become large
you may need to find another library support virtual-list, only render dom in visual range or implement a custom component by a virtual-library
I found a solution to the given problem, as #math-chen already stated the problem is the amount of data which will resolve in the actual Dom becoming really large. Rather than using virtual-lists, you can limit the amount of entries displayed which can easily be done by adding
limit:"10"
to the multiselect component, filtering all items can easily be handled by javascript itself.
Have started to play around with Vuex and am a bit confused.
It triggers the action GET_RECRUITERS everytime I load the component company.vue thus also making an api-call.
For example if I open company.vue => navigate to the user/edit.vue with vue-router and them go back it will call the action/api again (The recruiters are saved in the store accordinly to Vue-dev-tools).
Please correct me if I'm wrong - It should not trigger the action/api and thus resetting the state if I go back to the page again, correct? Or have I missunderstood the intent of Vuex?
company.vue
<template>
<card>
<select>
<option v-for="recruiter in recruiters"
:value="recruiter.id">
{{ recruiter.name }}
</option>
</select>
</card>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
mounted() {
this.$store.dispatch("company/GET_RECRUITERS")
},
computed: mapGetters({
recruiters: 'company/recruiters'
}),
}
</script>
company.js
import axios from 'axios'
// state
export const state = {
recruiters: [],
}
// getters
export const getters = {
recruiters: state => {
return state.recruiters
}
}
// actions
export const actions = {
GET_RECRUITERS(context) {
axios.get("api/recruiters")
.then((response) => {
console.log('API Action GET_RECRUITERS')
context.commit("GET_RECRUITERS", response.data.data)
})
.catch(() => { console.log("Error........") })
}
}
// mutations
export const mutations = {
GET_RECRUITERS(state, data) {
return state.recruiters = data
}
}
Thanks!
That's expected behavior, because a page component is created/mounted again each time you route back to it unless you cache it. Here are a few design patterns for this:
Load the data in App.vue which only runs once.
Or, check that the data isn't already loaded before making the API call:
// Testing that your `recruiters` getter has no length before loading data
mounted() {
if(!this.recruiters.length) {
this.$store.dispatch("company/GET_RECRUITERS");
}
}
Or, cache the page component so it's not recreated each time you route away and back. Do this by using the <keep-alive> component to wrap the <router-view>:
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.