Vue - Keep input focused on route change - javascript

I have a TopNavBar component, that is present on every route. This component includes a search input field. When a user clicks on the input field the route changes from /bar to /foo but input focus is lost. How can I (re)focus on the input?
TopNavBar.vue
<template>
<input type="search" name="search-library" v-focus ref="searchInput" #focus="initSearch". />
</template>
<script setup>
const searchInput = ref(null);
<input type="search" name="search-library" v-focus ref="searchInput" #focus="initSearch". />
function initSearch() {
if (router.currentRoute.value.name != "/foo") {
router.push({ path: "/foo", query: { initSearch: true }, key: route.fullPath });
}
}
watch(
() => router.currentRoute.value.path,
(newRoute) => {
if (newRoute == "/foo") {
searchInput.value.focus();
}
}
);
</script>
I'm using Vue3 and Nuxt3. v-focusz directive is declared globally in /plugins` folder and works as expected.
Update
TopNavBar is inside Nuxt 3 layout. Also, upon further investigation I've realised that the input does focus on route change but immediately loses it again.

You can achieve this by using $refs, Attach a reference on input element and then call focus method on it.
In template:
<parent-component>
<search-component ref="searchComponentRef" />
</parent-component>
In script:
mounted() {
this.$refs.searchComponentRef.$el.focus();
}

Related

How to pass a ref to a child input component with Vue?

I'm currently trying to pass a ref to get the value of the input (base-input component) on submit. You will find below the two components. With the console.log in handleSubmit, email is always undefined.
Thanks in advance for your help.
Parent component
<template>
<form #submit.prevent="handleSubmit">
<div class="flex flex-col mt-10">
<form-label forInput="email" label="Email Address" />
<base-input type="email" name="email" ref="email" />
</div>
</form>
</template>
<script>
import BaseInput from "../UI/BaseInput.vue";
export default {
components: {
BaseInput,
},
methods: {
handleSubmit() {
const email = this.$refs.email.value;
console.log(email);
},
},
};
</script>
Child Input component
<template>
<input
:type="type"
:name="name"
:ref="name"
/>
</template>
<script>
export default {
props: ["type", "name"],
};
</script>
If you want to access the value from the child's input field in the parent component, you need a way for data to flow from child to parent component, and that is done through emits.
But wouldn't it be nice to be able to use v-model with your custom BaseInput component? The same way one would use form input binding?
<input
:value="text"
#input="event => text = event.target.value">
or v-model to simplify
<input v-model="text">
Something like this
<BaseInput v-model="email" />
Well, we can do just that. What you need is a modelValue prop and update:modelValue emit event.
You can wrap both inside a writeable computed property for a cleaner and clearer syntax.
const props = defineProps({
modelValue: {
type: String,
},
});
const emit = defineEmits(['update:modelValue']);
const internalValue = computed({
get() {
return props.modelValue;
},
set(value: string) {
return emit('update:modelValue', value);
},
});
When internalValue is set to a new value, it emits that event to the parent component and it syncs it through props.modelValue. Meaning, you can change props.modelValue in the parent component and the change would be reflected inside your custom component. And vice versa.
I like this approach since it gives you a very natural way of reasoning about your component. Now, this concept isn't restricted to v-model per se, you can use it with any prop you want to synchronize to the parent component. Just use name prop, update:name emit in child component and use v-model:name in parent component.
Resources:
https://vuejs.org/guide/components/events.html#usage-with-v-model
first the BaseInput is spelt wrong in the template.
Instead of
<base-input type="email" name="email" ref="email" />
you need to change it to
<BaseInput :type="'email'" :name="'email'" ref="email" />
A way better approach will be to use #emit()
Child.vue
<template>
<input
:type="type"
:name="name"
#change="$emit('inputContent', Content)"
v-model="Content"
/>
</template>
<script>
export default {
emits: ['inputContent'],
data() {
return {
Content: '',
}
},
props: ["type", "name"],
};
</script>
Don't forget to declare your props as strings. 😉
Parent.vue
<template>
<BaseInput :type="'email'" :name="'email'" ref="email" #inputContent="handleSubmit"/>
</template>
<script>
import BaseInput from "../UI/BaseInput.vue";
export default {
components: {
BaseInput,
},
methods: {
handleSubmit(content) {
const email = content;
console.log(email);
},
},
};
</script>
Here is something about emits in the vue docs
and how to use v-model
I hope this helps :)

What's the best way to trigger a child emit from the parent component?

Right now I'm passing a trigger prop from the parent to child component, which triggers the emit from the child to the parent.
parent component:
<form #submit.prevent="state.store=true" method="post" enctype="multipart/form-data">
<child-component :triggerEmit=state.store #emitSomething="getSomething()"/>
child component:
const emit = defineEmits([
'emitBody'
])
watchEffect(async () => {
if (props.triggerEmit) {
emit('emitSomething', value)
}
})
This gets confusing quickly, if the components grow in size and I was wondering if there is a simpler way to trigger child emits from the parent, since this seems to be a common use case.
Edit:
Trying to trigger the child method directly from the parent (not working).
child:
const childMethod = () => {
console.log('check')
}
parent:
html:
<child ref="childRef"/>
script setup:
const childRef = ref()
childRef.value.childMethod()
Page throws error:
Cannot read properties of undefined (reading 'childMethod')
As per my understanding you want to access multiple child component methods/properties from the parent component. If Yes, you can achieve that by create a ref and access the methods.
In template :
<!-- parent.vue -->
<template>
<button #click="$refs.childComponentRef.childComponentMethod()">Click me</button>
<child-component ref="childComponentRef" />
</template>
In script :
With Vue 2 :
this.$refs.childComponentRef.childComponentMethod( );
With Vue 3 Composition Api :
setup( )
{
const childComponentRef = ref( );
childComponentRef.value.childComponentMethod( )
return {
childComponentRef
}
}
In this case, the parent's trigger is effectively querying the child for its event data so that it could call getSomething() on it. The parent already owns getSomething(), so it really only needs the child data.
Another way to get that data is to use v-model to track the child data:
In the child component, implement v-model for a prop (a string for example) by declaring a modelValue prop and emitting an 'update:modelValue' event with the new value as the event data:
<!-- ChildName.vue -->
<script setup>
defineProps({ modelValue: String })
defineEmits(['update:modelValue'])
</script>
<template>
<label>Name
<input type="text" :value="modelValue" #input="$emit('update:modelValue', $event.target.value)">
</label>
</template>
In the parent, add a reactive object, containing a field for each child's v-model:
<!-- ParentForm.vue -->
<script setup>
import { reactive } from 'vue'
const formData = reactive({
name: '',
age: 0,
address: {
city: '',
state: '',
},
})
</script>
<template>
<form>
<child-name v-model="formData.name" />
<child-age v-model="formData.age" />
<child-address v-model:city="formData.address.city" v-model:state="formData.address.state" />
<button>Submit</button>
</form>
</template>
Now, the parent can call getSomething() on each field upon submitting the form:
<!-- ParentForm.vue -->
<script setup>
import { toRaw } from 'vue'
â‹®
const getSomething = field => {
console.log('getting', field)
}
const submit = () => {
Object.entries(toRaw(formData)).forEach(getSomething)
}
</script>
<template>
<form #submit.prevent="submit">
â‹®
</form>
</template>
demo

How to DRY up VueJS + Vuelidate server side validation error code?

I'm working on a VueJS project (using the Quasar framework). When building a form component, for example to edit user settings, I have frontend validation, to check required fields etc. using vuelidate, and I want to also show backend validation errors. Currently my setup (for a sample form) is as follows:
script:
export default {
name: 'UserEditForm',
mixins: [formServerValidation],
data: function () {
return {
form: {
...
name: this.$store.state.currentUser.attributes.name,
currentPassword: 'winnerwinner',
serverErrors: {
// Todo: Autogenerate all server errors
}
}
}
},
methods: {
onSubmit () {
const name = this.form.name
const currentPassword = this.form.currentPassword
this.clearServerErrors()
this.$store.dispatch('updateUser', { name, currentPassword, }).then((result) => {
console.log("Server update success")
}).catch(err => {
console.log("Server update error")
const mappedErrors = this.mapServerErrors(err.response.data.errors)
merge(this.form.serverErrors, mappedErrors)
this.$refs.form.validate()
})
},
serverError: function (fieldName) {
return (value, vm) => {
return !(
Object.prototype.hasOwnProperty.call(vm, 'serverErrors') &&
Object.prototype.hasOwnProperty.call(vm.serverErrors, fieldName)
)
}
}
},
validation: {
form: {
...
name: {
required,
serverError: this.serverError('name')
},
currentPassword: {
required,
serverError: this.serverError('currentPassword')
},
...
}
}
}
template:
<template>
<q-form #submit="onSubmit" ref="form">
...
<q-input
field-name="name"
type="text"
v-model="form.name"
label="Name"
lazy-rules
#input="clearServerError('name', $event)"
:rules="[
val => $v.form.name.required || 'Enter name',
val => $v.form.name.serverError || form.serverErrors.name,
]" />
<q-input
field-name="currentPassword"
type="password"
v-model="form.currentPassword"
label="Current password*"
lazy-rules
#input="clearServerError('currentPassword', $event)"
:rules="[
val => $v.form.currentPassword.required || 'Confirm password',
val => $v.form.currentPassword.serverError || form.serverErrors.currentPassword
]"
/>
...
<div>
<q-btn label="Save" type="submit" color="primary" />
</div>
</q-form>
</template>
This all works perfectly fine, however it seems to be very WET (not DRY). I have to define the following things for each field manually:
serverError: this.serverError('name') in validation
A rule val => $v.form.name.serverError || form.serverErrors.name in the template
#input="clearServerError('name', $event)" in the template
But I know I want to do this for every input in my form component, so it feels very repetitive to do this manually for every input component. Is there a correct "Vue-ish" way to DRY this up?
One thing I tried is to find all input fields by traversing all descendants of my form component using $children. Then:
I tried to tackle 1., by defining these server errors dynamically by looping over all input components.
2. is harder to tackle since you can not directly update the property :rules since Vue will override it on a new render of the parent component. (This also throws a warning, saying you should not directly update properties, but use v-model etc.)
I tried to tackle 3. by defining input event listeners dynamically from my Form component using the $on method on the actual input components.
The DRY-up for 1 and 3 seem to work, but is it the best way of doing it? And what is a correct approach of drying up 2?

v-on:change not calling method in nuxt.js

This seems like a simple thing to do so I'm not exactly sure what I'm doing wrong here, I followed this question how to fire an event when v-model changes
but I cant seem to get it to work..
This is my component
<template>
<div>
<input type="text" v-model="searchTerm" v-on:change="search" />
</div>
</template>
<script>
export default {
data() {
return {
searchTerm: ''
}
},
methods: {
search() {
console.log(this.searchTerm);
}
}
}
</script>
now basically what I'm trying to do is when a user starts typing in the input, fire this event and console.log() the updated searchTerm, but when I start typing nothing is logged to the console??
Am I doing something wrong? Is this not how you listen to v-model changes in nuxt?
Try to use #input instead of #change event like so :
<template>
<div>
<input type="text" v-model="searchTerm" v-on:input="search" />
</div>
</template>

Setting focus of an input element in vue.js

I'm trying to set the focus of an input element in Vue.js. I found some help online but none of the explanation worked for me.
Here's my code :
<template>
<form method="post" action="" v-on:submit.prevent="search">
<input type="text" placeholder="Person name" required v-model="name" v-el="nameInput" />
<input type="text" placeholder="Company" required v-model="company" v-el="domainInput" />
<input type="submit" value="Search" class="btn show-m" />
</form>
</template>
<script>
export default {
data () {
return {
contacts: [],
name: null,
company: null
}
},
ready: {
// I tried the following :
this.$$.nameInput.focus();
this.$els.nameInput.focus();
// None of them worked !
}
methods: {
search: function (event) {
// ...
// I also would like to give the focus here, once the form has been submitted.
// And here also, this.$$ and this.$els doesn't work
},
}
}
</script>
I tried this.$$.nameInput.focus(); and this.$els.nameInput.focus(); for what I could find online to target the focus, but this.$$ is undefined, and this.$els is empty.
If that can help, I'm using vue.js v1.0.15
Thank you for your help.
In vue 2.x you can solve it with a directive.
Vue.directive('focus', {
inserted: function (el) {
el.focus()
}
})
Then you can use v-focus attribute on inputs and other elements:
<input v-focus>
Another solution using Vue 2.x and ref.
You can use the ref/$refs attribute to target your input and focus it.
In the example a simple method is used which can target the inputs using the ref attribute supplied to the inputs.
Then access the $refs property on your instance to get a reference to the DOM element.
<script>
export default {
// ...
mounted: function () {
this.focusInput('nameInput');
},
methods: {
// This is the method that focuses the element
focusInput: function ( inputRef ) {
// $refs is an object that holds the DOM references to your inputs
this.$refs[inputRef].focus();
},
search: function (event) {
this.focusInput('domainInput');
},
}
}
</script>
<template>
<form method="post" action="" v-on:submit.prevent="search">
<input type="text" placeholder="Person name" required v-model="name" ref="nameInput" />
<input type="text" placeholder="Company" required v-model="company" ref="domainInput" />
<input type="submit" value="Search" class="btn show-m" />
</form>
</template>
This solution is best for a one off situation or for a reusable component. For a more global approach the directive is the way to go.
Setting focus inside a child element
(for those of you that struggled for hours like me)
Parent:
<template>
<div #click="$refs.theComponent.$refs.theInput.focus()">
<custom-input ref="theComponent"/>
</div>
</template>
Child (CustomInput.vue):
<template>
<input ref="theInput"/>
</template>
There are a couple of issues.
First of all, v-els are defined like this:
<input v-el:input-element/>
That'll turn the variable to a camelCase in the code. You can read up more on this weird functionality here.
Other than that, you should be able to access the variable through this.$els.inputElement. Mind you, it will only appear in the component that you're defining that element (or the main app itself, if you defined it there).
Secondly, the automatic focusing does not seem to be working on Firefox (43.0.4), at least on my machine. Everything works great on Chrome, and focuses as expected.
Using ref I managed to focus an Input on mounted like this.
Template :
<b-form-input v-model="query" ref="searchInput" ></b-form-input>
Javascript :
mounted(){
this.$refs.searchInput.$el.focus()
}
Vue 3.x
Use a custom directive.
app.directive('focus', {
mounted(el) {
el.focus()
}
})
Here is how you use it:
Step 1:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.directive('focus', {
mounted(el) { // When the bound element is inserted into the DOM...
el.focus() // Focus the element
}
})
/* Optional:
Add a slight delay if the input does not focus.
app.directive('focus', {
mounted(el) { // When the bound element is inserted into the DOM...
setTimeout(() => {
el.focus() // Focus the element
}, 500)
}
}) */
await router.isReady()
app.mount('#app')
Then in your component:
Step 2:
// MyInput.vue
<input v-focus>
Vue docs
According to vue 2.x, you can also register "directive" locally in component to get autofocus.
just write directive in component:
export default {
directives: { focus: {
inserted: function (el) {
el.focus()
}
}
}
}
Then in a template, you can use the new v-focus attribute on any element, like this:
<input type="text" v-focus>

Categories

Resources