Multiple forms with one submit() method by Vue - javascript

I have a few forms. Every of them have the same logic (validation, sending...) so, I want to create one method to control actions on my forms. For now my code is redundancy, because I have the same methods onSubmit() on every .vue file.
my HTML:
<div id="app">
<myform-one></myform-one>
<myform-two></myform-two>
</div>
my JavaScript (main.js - entry file in webpack):
import Vue from 'vue';
import Myform1 from './myform1.vue';
import Myform2 from './myform2.vue';
new Vue({
el: '#app',
components: {
myformOne: Myform1,
myformTwo: Myform2
}
});
and VUE components files:
myform1.vue:
<template>
<div>
<form #submit.prevent="onSubmit">
<input type="text" v-model="fields.fname11" />
<input type="text" v-model="fields.fname12" />
<button type="submit">submit</button>
</form>
</div>
</template>
<script>
let formfields = {
fname11: '',
fname12: ''
};
export default {
data() {
return {
fields: formfields
}
},
methods: {
onSubmit() {
// code responsible for reading, validating and sending data here
// ...
console.log(this.fields);
}
},
}
</script>
and myform2.vue:
<template>
<div>
<form #submit.prevent="onSubmit">
<input type="text" v-model="fields.fname21" />
<input type="text" v-model="fields.fname22" />
<input type="text" v-model="fields.fname23" />
<button type="submit">submit</button>
</form>
</div>
</template>
<script>
let formfields = {
fname21: '',
fname22: '',
fname23: '',
};
export default {
data() {
return {
fields: formfields
}
},
methods: {
onSubmit() {
// code responsible for reading, validating and sending data here
// ...
console.log(this.fields);
}
},
}
</script>
How can I create and use one, common method submitForm()? And where its code should be (good practice)?

Create a separate file which contains the logic:
// submitForm.js
export default function (fields) {
// code responsible for reading, validating and sending data here
// ...
}
Then use that logic inside the components
import submitForm from "../services/submitForm.js"
...
methods: {
onSubmit() {
submitForm(this.fields)
}
}

Vue3 (with Quasar for me but I'm sure it would work for any framework):
Say you have a parent which contains a number of forms <Forms />:
First create a composable function like so useForms.js:
import { ref } from 'vue'
const forms = ref([])
export function useForms(){
const checkForms = () => {
forms.value.forEach((form) => form.validate()
}
const addFormToFormsArray = (form) => {
forms.value.push(form)
}
return { forms, addFormToFormsArray, checkForms }
}
Then import it into <Forms />:
<template>
<Form />
<Form />
<Form />
<button #click="checkForms">Check Form</button>
</template>
<script setup>
import { useForms } from '../useForms';
const { checkForms } = useForms()
</script>
Finally, inside the <Form />:
<template>
<form ref="form">
.../stuff
</form>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useForms } from '../useForms';
const form = ref(null)
onMounted(() => {
addFormToFormsArray(form.value)
})
const { checkForms, addFormToFormsArray } = useForms()
</script>
When performing the check function in the parent, it should go through each form and check for any issues.

There are some options. My favorite is creating a mixin vue docs mixins
export const form_functionality = {
methods: {
on_submit() {
//logic of submit
},
//here we can have other reusable methods
}
}
Then in your components use that mixin as follow:
import { form_functionality } from 'path_of_mixin'
export default {
mixins: [form_functionality]
}
In the end, what mixins has (created, methods, data etc) will be merged to the component
which uses that mixin.
So, practically you can access the mixin method like this.on_submit()

Related

Vue: Teleport inheritAttrs false not working

I'm trying to create a BaseOverlay component that basically teleports its content to a certain area of my application. It works just fine except there's an issue when using it with v-show... I think because my component's root is a Teleport that v-show won't work because Teleport is a template.
I figured I could then use inheritAttrs: false and v-bind="$attrs" on the inner content... this throws a warning from Vue saying Runtime directive used on component with non-element root node. The directives will not function as intended. It results in v-show not working on MyComponent, but v-if does work.
Any clues as to what I'm doing wrong?
Example
App.vue
<script setup>
import MyComponent from "./MyComponent.vue";
import {ref} from "vue";
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div id="overlays" />
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Doesn't work" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
BaseOverlay.vue
<template>
<Teleport to="#overlays">
<div
class="overlay-container"
v-bind="$attrs"
>
<slot />
</div>
</Teleport>
</template>
<script>
export default {
name: "BaseOverlay",
inheritAttrs: false,
};
</script>
MyComponent.vue
<template>
<BaseOverlay>
{{text}}
</BaseOverlay>
</template>
<script>
import BaseOverlay from "./BaseOverlay.vue";
export default {
name: "MyComponent",
components: {
BaseOverlay
},
props: {
text: {
type: String,
default: ""
}
}
}
</script>
I would consider moving the modal/overlay dependency out of the component and into the app composition to make it more reusable.
Note the isMounted check check - this is to add a delay if the outlet containers have not yet been defined. You may need to add additional handling if your outlets are not pressent on mount e.g. <div id="outlet" v-if="false">
const {
createApp,
defineComponent,
ref,
onMounted
} = Vue
//
// Modal
//
const MyModal = defineComponent({
name: 'MyModal',
props: {
to: {
type: String,
required: true
},
show: Boolean
},
setup(){
const isMounted = ref(false);
onMounted(()=> isMounted.value = true )
return { isMounted }
},
template: `
<teleport :to="to" v-if="isMounted">
<div class="overlay-container" v-show="show">
<slot />
</div>
</teleport>
`
})
//
// Component
//
const MyComp = defineComponent({
name: 'MyComp',
template: `<div>Component</div>`
})
//
// App
//
const MyApp = defineComponent({
name: 'MyApp',
components: {
MyModal,
MyComp
},
setup() {
const modalShow = ref(false);
const modalOutlet = ref('#outletOne');
const toggleModalShow = () => {
modalShow.value = !modalShow.value
}
const toggleModalOutlet = () => {
modalOutlet.value = modalOutlet.value == '#outletOne'
? '#outletTwo'
: '#outletOne'
}
return {
toggleModalShow,
modalShow,
toggleModalOutlet,
modalOutlet,
}
},
template: `
<div>
<button #click="toggleModalShow">{{ modalShow ? 'Hide' : 'Show' }} Modal</button>
<button #click="toggleModalOutlet">Toggle Modal Outlet {{ modalOutlet }} </button>
</div>
<MyModal :to="modalOutlet" :show="modalShow">
<MyComp />
</MyModal>
<div id="outletOne">
<h2>Outlet One</h2>
<!-- outlet one -->
</div>
<div id="outletTwo">
<h2>Outlet Two</h2>
<!-- outlet two -->
</div>
`
})
//
// Assemble
//
const app = createApp(MyApp)
app.mount('body')
/* just some styling */
#outletOne { color: tomato; }
#outletTwo { color: olive; }
h2 { margin: 0; }
[id*=outlet]{ display: inline-flex; flex-direction: column; padding: 1rem; }
button { margin: 1rem; }
<script src="https://unpkg.com/vue#3.1.4/dist/vue.global.js"></script>
Wanted to follow-up on this. I started running into a lot of other issues with Teleport, like the inability to use it as a root element in a component (like a Dialog component because I want to teleport all dialogs to a certain area of the application), and some other strange issues with KeepAlive.
I ended up rolling my own WebComponent and using that instead. I have an OverlayManager WebComponent that is used within the BaseOverlay component, and every time a BaseOverlay is mounted, it adds itself to the OverlayManager.
Example
OverlayManager.js
export class OverlayManager extends HTMLElement {
constructor() {
super();
this.classList.add("absolute", "top-100", "left-0")
document.body.appendChild(this);
}
add(element) {
this.appendChild(element);
}
remove(element) {
this.removeChild(element);
}
}
customElements.define("overlay-manager", OverlayManager);
BaseOverlay.vue
<template>
<div class="overlay-container" ref="rootEl">
<slot />
</div>
</template>
<script>
import {ref, onMounted, onBeforeUnmount, inject} from "vue";
export default {
name: "BaseOverlay",
setup() {
const rootEl = ref(null);
const OverlayManager = inject("OverlayManager");
onMounted(() => {
OverlayManager.add(rootEl.value);
});
onBeforeUnmount(() => {
OverlayManager.remove(rootEl.value);
});
return {
rootEl
}
}
};
</script>
App.vue
<script setup>
import {OverlayManager} from "./OverlayManager.js";
import MyComponent from "./MyComponent.vue";
import {ref, provide} from "vue";
const manager = new OverlayManager();
provide("OverlayManager", manager);
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Now Works" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
<style>
.absolute {
position: absolute;
}
.top-100 {
top: 100px;
}
.left-0 {
left: 0;
}
</style>
This behaves exactly how I need it, and I don't have to deal with the quirks that Teleport introduces, and it allows me to have a singleton that is in charge of all of my overlays. The other benefit is that I have access to the parent of where BaseOverlay is initially added in the HTML (not where it's moved). Honestly not sure if this is a good practice, but I'm chuffed at how cute this is and how well Vue integrates with it.

How to make "two-way binding" Vue

I have child component which function is to upload a photo. Uploaded photo assigned to child component data named "photo".
I need to bind parent data named "file" with child data named "photo". And when "photo" is changed "file" should be changed too.
Child component:
<template>
<div class="select">
<img v-if="previewFile" :src="previewFile" alt="" />
<img v-else src="/images/empty.jpg" alt="" />
<label class="btn" for="image-upload">{{ btnLabel }}</label>
<input id="image-upload" type="file" ref="file" #change="uploadFile" />
</div>
</template>
import { mapGetters } from "vuex";
export default {
name: "SelectPhoto",
data() {
return {
file: null,
previewFile: null,
};
},
methods: {
uploadFile() {
this.file = this.$refs.file.files[0];
}
}
}
Parent component:
<template>
<SelectPhoto :btn-label="text.RU.photoSelect.choosePhoto" v-model:file.sync="file"/>
<BaseButton :label="text.RU.photoSelect.knowName" #do="$emit('getResult', file, previewFile)" />
</template>
<script>
export default {
data() {
return {
file: null,
};
},
};
</script>
You already used $emit on your BaseButton component. You could also use it for this.file. In your child component like this:
uploadFile() {
this.file = this.$refs.file.files[0];
this.$emit('sendMyFile', this.file)
}
In you parent component, to react to the action:
<SelectPhoto #sendMyFile="getMyFile" :btn-label="text.RU.photoSelect.choosePhoto" v-model:file.sync="file"/>
Also in the parent component do what you want with the the.file:
methods: {
getMyFile(file) {
// do something
}
}

Composition API with Nuxt 2 to get template refs array

I'm trying to get the array of element refs that are not in v-for. I'm using #nuxtjs/composition-api on Nuxt 2.
(Truth: I want to make an array of input elements, so that I can perform validations on them before submit)
This sounds too easy on vue 2 as $refs becomes an array when one or more compnents have the same ref name on html. However, this doesn't sound simple with composition api and trying to perform simple task with that got me stuck from long.
So to handle this scenario, I've created 1 composable function. (Soruce: https://v3-migration.vuejs.org/breaking-changes/array-refs.html#frontmatter-title)
// file: viewRefs.js
import { onBeforeUpdate, onUpdated } from '#nuxtjs/composition-api'
export default () => {
let itemRefs = []
const setItemRef = el => {
console.log('adding item ref')
if (el) {
itemRefs.push(el)
}
}
onBeforeUpdate(() => {
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs)
})
return {
itemRefs,
setItemRef
}
}
Here is my vue file:
<template>
<div>
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
// rest of my cool html
</div>
</template>
<script>
import {
defineComponent,
reactive,
useRouter,
ref
} from '#nuxtjs/composition-api'
import viewRefs from '~/composables/viewRefs'
export default defineComponent({
setup() {
const input = viewRefs()
// awesome vue code here...
return {
input
}
}
})
</script>
Now when I run this file, I don't see any adding item ref logs. And on click of a button, I'm logging input. That has 0 items in the itemRefs array.
What's going wrong?
Nuxt 2 is based on Vue 2, which only accepts strings for the ref attribute. The docs you linked actually refer to new behavior in Vue 3 for ref, where functions are also accepted.
Template refs in Nuxt 2 work the same way as they do in Vue 2 with Composition API: When a ref is inside a v-for, the ref becomes an array:
<template>
<div id="app">
<button #click="logRefs">Log refs</button>
<input v-for="i in 4" :key="i" ref="itemRef" />
</div>
</template>
<script>
import { ref } from '#vue/composition-api'
export default {
setup() {
const itemRef = ref(null)
return {
itemRef,
logRefs() {
console.log(itemRef.value) // => array of inputs
},
}
}
}
</script>
demo
And setup() does not provide access to $refs, as template refs must be explicitly declared as reactive refs in Composition API.

Vue composition API calling child component method

TLDR; In v2, this.$refs does the job but how can I do that in v3 composition api?
I am trying to use CustomUpload feature of PrimeVue in Vue3, but that API does not clear the upload files after uploading them and I need to call clear() method of the child component in the parent component to clear the files and refresh the button.
Here's my App.vue
<template>
<FileUpload
name="upload"
url="/"
mode="basic"
:auto="true"
:maxFileSize="26214400"
:fileLimit="1"
:customUpload="true"
#uploader="upload"
/>
<Button name="lalaal">qweeq</Button>
</template>
<script>
import FileUpload from 'primevue/fileupload'
export default {
setup() {
const upload = e => {
console.log('testing', e)
}
return { upload }
},
components: {
FileUpload
}
}
</script>
Thanks
You could use template ref then use uploadFile.value instead of this.$refs.uploadFile :
<template>
<FileUpload
ref="uploadFile"
name="upload"
url="/"
mode="basic"
:auto="true"
:maxFileSize="26214400"
:fileLimit="1"
:customUpload="true"
#uploader="upload"
/>
<Button name="lalaal">qweeq</Button>
</template>
<script>
import FileUpload from 'primevue/fileupload'
import {ref} from "vue";
export default {
setup() {
const uploadFile=ref(null)
const upload = e => {
console.log('testing', e)
}
return { upload,uploadFile}
},
components: {
FileUpload
}
}
</script>

nuxt.js passing prop to component's inner element

I have a simple component:
<template>
<div id="search__index_search-form">
<input :foo-id="fooId" #keyup.enter="findFoos()" type="text" :value="keyword" #input="updateKeyword"
placeholder="Search for a foo">
<button #click="findFoos()">Search!</button>
{{fooId}}
</div>
</template>
<script>
import {mapState} from "vuex";
export default {
computed: mapState({
keyword: state => state.search.keyword
}),
data: function () {
return {fooId: "all"};
},
methods: {
updateKeyword(e) {
this.$store.commit("setSearchKeyword", e.target.value);
},
findFoos() {
this.$store.dispatch("findFoos");
}
}
};
</script>
I am calling it from nuxt page:
<template>
<searchbar v-bind:fooId="500"/>
</template>
<script>
import searchBar from "~/components/search-bar.vue";
export default {
components: {
'searchbar': searchBar
}
};
</script>
This results in:
<div id="search__index_search-form" fooid="500"><input shop-id="all" type="text" placeholder="Search for a foo"><button>Search!</button>
all
</div>
Question is, why is fooId bound to "div.search__index_search-form" and not to input? And how come {{fooId}} results in "all" (default state), and not "500"?
fooId is rendered on div#search__index_search-form because you do not declare fooId as a property of the component. Vue's default behavior is to render undeclared properties on the root element of the component.
You need to declare fooId as a property.
export default {
props: {
fooId: {type: String, default: "all"}
},
computed: mapState({
keyword: state => state.search.keyword
}),
methods: {
updateKeyword(e) {
this.$store.commit("setSearchKeyword", e.target.value);
},
findProducts() {
this.$store.dispatch("findFoos");
}
}
};
I'm not sure what you are really trying to accomplish though.
<input :foo-id="fooId" ... >
That bit of code doesn't seem to make any sense.

Categories