Issue
I'm trying to extend a vuetify VTextField component to create a reusable password-field. There are a number of props that control the component that we need to mutate. Vuejs considers prop mutation an "anti-pattern" and warnings against it.
I've experimented with declaring a computed-property that overrides the prop which works, but it tosses warnings in the web-console about the conflict.
Here is a simple example:
import Vue from 'vue'
import { VTextField } from 'vuetify/lib'
export default Vue.extend({
name: 'password-field',
mixins: [VTextField],
data: () => ({
reveal: false
}),
computed: {
function type () {
return this.reveal ? 'text' : 'password'
}
}
})
It feels like there should be away to use mixins to extend the VTextField and selectively drop the props we want to replace with computed-properties. In the end, we need the value to be reactive and under the control of the password-field component -- not controlled by the parent.
Am I going the on the wrong direction here?
UPDATED
With the expert advice provided by Yom S (), I was able to create a custom extension of VTextField. We went with his suggestion #2, an SFC templated component.
For anyone else who stumbles across this topic, here is the Typescript compatible implementation:
<!-- put this in components/password-field.vue -->
<template>
<v-text-field
v-bind="computedProps"
v-on:click:append="reveal = !reveal"
v-on="listeners$"
></v-text-field>
</template>
<script lang="ts">
import Vue from 'vue'
import { VTextField } from 'vuetify/lib'
export default {
name: 'PasswordField',
extends: VTextField,
props: {
label: {
type: String,
default: 'Password'
},
rules: {
type: Array,
default: () => [(v: string) => {
return /((?=.*\d)(?=.*[a-z])(?=.*[!@#$%^&*()?.]).{8,})/i.test(v) ||
'At least 8 char; upper and lowercase, a number and a special char'
}]
}
},
data: () => ({
reveal: false
}),
computed: {
computedProps: function () {
return {
...this.$props,
type: this.reveal ? 'text' : 'password',
appendIcon: this.reveal ? 'mdi-eye' : 'mdi-eye-off'
}
}
}
} as Vue.ComponentOptions<Vue>
</script>
Here's a simple example of how to use this component
<template>
<v-form v-model="formValid">
<password-field v-model="newPassword/>
<v-btn :disabled="!formValid">Change</v-btn>
</v-form>
</template>
<script lang="ts">
import Vue from 'vue'
import PasswordField from '@/components/password-field.vue'
export default Vue.extend({
name: 'ChangePasswordForm',
data: () => ({
formValid: false,
newPassword: ''
})
})
</script>
Solution
It would've been helpful if this particular type prop was sync-able; but since it's not, you could work around this by sort of re-rendering the VTextField, while also extending from it.
Now, I can't say this is the best solution as it has some drawbacks that make it a flawed wrapper. But it does get you what you need, as per your requirement in question.
Common cons:
- Scoped slots (e.g.
append,append-outer) will not output the expected element(s).
So for this purpose, let's call this component "PasswordField", and we would use it like:
<PasswordField
label="Enter your password"
:revealed="revealed"
append-outer-icon="mdi-eye"
@click:append-outer="togglePassword" />
The append-outer-icon and the icon-toggling mechanism should probably be encapsulated within the component itself.
And here goes the implementation:
PasswordField.js
- Pros:
- Somewhat simpler (as in no template necessary).
- Faster compilation time, since it's just a JS file and won't have to go through the Vue template compiler and Vue Loader. (You could go further and make it a functional component).
- Cons:
- Event listeners don't seem to work.
import { VTextField } from 'vuetify/lib';
export default {
name: 'PasswordField',
extends: VTextField,
props: {
revealed: {
type: Boolean,
default: false
}
},
render(h) {
const { revealed, ...innerProps } = this.$options.propsData;
return h(VTextField, {
// For some reason this isn't effective
listeners: this.$listeners,
props: {
...innerProps,
type: revealed ? 'text' : 'password'
}
})
}
}
Notice this extends from the base component (VTextField) and sort of "overrides" the original render function, returning customized virtual node a.k.a. VNode.
However, as mentioned earlier, this has some drawback where it fails to listen to emitted events. (I would love to know if someone has a solution for this).
So, as a last resort, let's actually use a template and computed props, literally (we want the props part being the only properties to bind to, minus the data).
PasswordField.vue
- Pros:
- More reliable, functionality-wise.
- Event listeners will work as they should.
- And of course, SFC works best this way.
- Cons:
- Somewhat repetitive, in that you'd have to manually (re)bind props and register events.
- Slower compilation time (should be hardly noticeable anyway).
<template>
<v-text-field
v-bind="computedProps"
v-on="$listeners">
</v-text-field>
</template>
<script>
import { VTextField } from 'vuetify/lib';
export default {
name: 'PasswordField',
extends: VTextField,
props: {
revealed: {
type: Boolean,
default: false
}
},
computed: {
computedProps() {
return {
...this.$props,
type: this.revealed ? 'text' : 'password'
}
}
}
}
</script>
Hope that helps in some way!
Answered By - Yom T.
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.