In an existing code base the ElementUI and Vue2 packages got updated. The users are relying on what looks to be unintended behavior from the outdated DatePicker UI component.
The date is visually formatted as 30/01/2022 in the input field of the DatePicker elements.
To speed up typing users were entering 30012022 into the field instead. This behavior disappeared after the updates. Only entering the date with the / separator gets accepted.
How do I overwrite the vendor methods of the ElementUI Date Time component to ease the input validation?
Replacing validation can be done by wrapping the el-date-picker into your own component. The trick is to use $nextTick inside mounted() and then access the method you want to replace through a reference on the wrapped component.
A small code example of the .vue file using TypeScript:
<template>
<el-date-picker
ref="customDatePicker"
:type="type"
:size="size"
:value="value"
:clearable="clearable"
:format="displayFormat"
:value-format="valueFormat" />
</template>
<script lang="ts">
import moment from 'moment'
import { Component, Prop, Vue } from 'vue-property-decorator'
type PickerType = 'year' | 'month' | 'date' | 'dates' | 'datetime' | ' week' | 'datetimerange' | 'daterange' | 'monthrange'
type Size = 'large' | 'small' | 'mini'
#Component
export default class RexDatePicker extends Vue {
#Prop()
readonly value: any
#Prop({ type: String, default: 'small' })
readonly size!: Size
#Prop({ type: String, default: 'date' })
readonly type!: PickerType
#Prop({ type: String, default: 'yyyy-MM-dd' })
readonly valueFormat!: string
data() {
return {
clearable: this.$attrs.clearable !== 'false'
}
}
get displayFormat() : string {
if (this.type === 'year') {
return 'yyyy'
}
return 'dd/MM/yyyy'
}
mounted() {
this.$nextTick(() => {
var elPicker: any = this.$refs.customDatePicker
if (this.type === 'date' || this.type === 'daterange') {
// inject custom date input behavior
const originalParseFunction = elPicker.parseString
elPicker.parseString = function (value) {
value = expandNumbersToDate(value)
return originalParseFunction(value)
}
}
})
}
}
function expandNumbersToDate(value) {
// expects String, Date or an Array of those two
if (Object.prototype.toString.call(value) === '[object String]') {
var currentMonth = moment().format('MM')
var currentYear = moment().format('YYYY')
value = value.replace(/^[^\d]*(\d{2})[^\d]*$/, `$1/${currentMonth}/${currentYear}`)
value = value.replace(/^[^\d]*(\d{2})(\d{2})[^\d]*$/, `$1/$2/${currentYear}`)
value = value.replace(/^[^\d]*(\d{2})\/?(\d{2})\/?(\d{4})[^\d]*$/, '$1/$2/$3')
}
if (Array.isArray(value)) {
value = value.map(date => expandNumbersToDate(date))
}
return value
}
</script>
Related
I'm using Pinia as Store for my Vue 3 application. The problem is that the store reacts on some changes, but ignores others.
The store looks like that:
state: () => {
return {
roles: [],
currentRole: 'Administrator',
elements: []
}
},
getters: {
getElementsForCurrentRole: (state) => {
let role = state.roles.find((role) => role.label == state.currentRole);
if (role) {
return role.permissions.elements;
}
}
},
In the template file, I communicate with the store like this:
<template>
<div>
<draggable
v-model="getElementsForCurrentRole"
group="elements"
#end="onDragEnd"
item-key="name">
<template #item="{element}">
<n-card :title="formatElementName(element.name)" size="small" header-style="{titleFontSizeSmall: 8px}" hoverable>
<n-switch v-model:value="element.active" size="small" />
</n-card>
</template>
</draggable>
</div>
</template>
<script setup>
import { NCard, NSwitch } from 'naive-ui';
import draggable from 'vuedraggable'
import { usePermissionsStore } from '#/stores/permissions';
import { storeToRefs } from 'pinia';
const props = defineProps({
selectedRole: {
type: String
}
})
const permissionsStore = usePermissionsStore();
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const onDragEnd = () => {
permissionsStore.save();
}
const formatElementName = (element) => {
let title = element.charAt(0).toUpperCase() + element.slice(1);
title = title.replace('-', ' ');
title = title.split(' ');
if (title[1]) {
title = title[0] + ' ' + title[1].charAt(0).toUpperCase() + title[1].slice(1);
}
if (typeof title == 'object') {
return title[0];
}
return title;
}
</script>
My problem is the v-model="getElementsForCurrentRole". When making changes, for example changing the value for the switch, the store is reactive and the changes are made successfully. But: If I try to change the Array order by dragging, the store does not update the order. I'm confused, because the store reacts on other value changes, but not on the order change.
What can be the issue here? Do I something wrong?
-Edit- I see the following warning on drag: Write operation failed: computed value is readonly
Workaround
As workaround I work with the drag event and write the new index directly to the store variable. But...its just a workaround. I would really appreciate a cleaner solution.
Here is the workaround code:
onDrag = (event) => {
if (event && event.type == 'end') {
// Is Drag Event. Save the new order manually directly in the store
let current = permissionsStore.roles.find((role) => role.value == permissionsStore.currentRole);
var element = current.permissions.elements[event.oldIndex];
current.permissions.elements.splice(event.oldIndex, 1);
current.permissions.elements.splice(event.newIndex, 0, element);
}
}
You should put reactive value on v-model.
getElementsForCurrentRole is from getters, so it is treated as computed value.
Similar to toRefs() but specifically designed for Pinia stores so
methods and non reactive properties are completely ignored.
https://pinia.vuejs.org/api/modules/pinia.html#storetorefs
I think this should work for you.
// template
v-model="elementsForCurrentRole"
// script
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const elementsForCurrentRole = ref(getElementsForCurrentRole.value);
I have a field defined as follows:
<Field
class="form-control form-control-solid"
:value="tradeSizeFactor"
name="tradeSizeFactor"
:v-model="tradeSizeFactor"
/>
In my setup I watch if the value of property changed and if so I get the new value for for example the tradeSizeFactor:
setup(props)
{
const copyAccountId = ref(props.copyAccountId);
const copyAccountName = ref(props.copyAccountName);
let tradeSizeFactor = ref(0);
watchEffect(() => {
console.log(`watch Effect id: ${props.copyAccountId}`);
console.log(`watch Effect name: ${props.copyAccountName}`);
copyAccountId.value = props.copyAccountId;
if (props.copyAccountId !== 0) {
getSettings(props.copyAccountId);
}
});
async function getSettings(copyAccountId) {
store
.dispatch(Actions.GET_COPYACCOUNTSETTINGS, copyAccountId)
.then((data) => {
console.log(
`data: ${JSON.stringify(data.data.settings.tradeSizeFactor)}`
);
Object.assign(copyAccountSettings.value, data.data.settings);
tradeSizeFactor = data.data.settings.tradeSizeFactor;
});
}
return {
tradeSizeFactor,
};
}
Whatever I try however, the value of tradeSizeFactor is not updating in the Field. It keeps showing 0...
In the following line, you're incorrectly overwriting the ref with a literal:
tradeSizeFactor = data.data.settings.tradeSizeFactor;
^^^^^^^^^^^^^^^
tradeSizeFactor is a ref, so the value must be changed via its value property:
tradeSizeFactor.value = data.data.settings.tradeSizeFactor;
👆
i want to pass a String to my child component like this, but previously i want to print it
this is my parent component
{{left.leftA}} // here show me 8
<CustomCard
:left="left.leftA"
export default {
name: 'Parent',
data() {},
setup() {
onMounted(async () => {
const answer = await getData(name)
left.value = answer.response //{leftA:'A', leftB:'B'...}
})
and in my child component i have this declaration
export default {
name: 'CustomCard',
props: {
left: {
type: String,
required: true,
},
i am getting this warning:
[Vue warn]: Invalid prop: type check failed for prop "left". Expected String with
value "undefined", got Undefined
Does it have something to do with how I am loading the data? is it ok to use onMounted?
This is happening because the initial value for value is null. So, on initial render it throws the warning, but upon another render it has the correct prop type (a string) and renders correctly.
You have 3 options. Allow '' as an option on the prop or don’t render the component until you have the correct data or make use of computed Property.
Option-1
{{left.leftA}} // here show me 8
<CustomCard
:left="left.leftA ? left.leftA : ''"
Option-2
{{left.leftA}} // here show me 8
<CustomCard v-if="loaded"
:left="left.leftA"
and in onMounted(}
onMounted(async () => {
const answer = await getData(name)
left.value = answer.response //{leftA:'A', leftB:'B'...}
// Set the loaded flag as true here. Also make sure its set as false inside the setup()
})
Option-3
{{left.leftA}} // here show me 8
<CustomCard
:left="sendVal"
In computed....
computed: {
sendVal() {
if(left && left.left1) return left.left1;
return '';
}
}
I am new to Vue.js and am trying to create components that will simplify form creation, based on a library I have been using for a while now (PHP).
I have created a component that renders a label + textbox, styled via Bootstrap.
In order to avoid having to pass all the parameters every time, I want to be able to define defaults from within the parent, so that they will stay in effect until changed.
The component looks like this (MyTextBox.vue)
<template>
<div v-bind:class="myDivWidth">
<label v-bind:class="`control-label ${myLabelWidth}`">{{label}}</label>
<div v-bind:class="myControlWidth">
<input class="form-control col-md-12" v-bind:value="value">
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
// trying to use this as 'class' variable but most likely wrong
myDefaultLabelWidth: 4
}
},
props: {
label: String,
labelWidth: String,
controlWidth: String,
divWidth: String,
value: {required: false},
defaultLabelWidth: {type: String}
},
computed: {
myLabelWidth: function () {
let lw;
//debugger;
do {
if (typeof this.defaultLabelWidth !== 'undefined') {
lw = this.defaultLabelWidth;
// ****** Note the call to the parent function
this.$parent.setDefault('defaultLabelWidth', lw);
break;
}
if (typeof this.labelWidth !== 'undefined') {
lw = this.labelWidth;
break;
}
if (typeof this.lw !== 'undefined') {
lw = this.lw;
break;
}
// ****** Note the call to the parent function
lw = this.$parent.getDefault('defaultLabelWidth');
} while (false);
return `col-md-${lw}`;
},
// snip....
}
}
</script>
and it is used like this (I am only showing attributes relating to label, for brevity)
(StoryEditor.vue)
<my-textbox label="LableText1" default-label-width=4></my-textbox>
<my-textbox label="LableText2"></my-textbox>
<my-textbox label="LableText3" label-width=5></my-textbox>
<my-textbox label="LableText4"></my-textbox>
<my-textbox label="LableText5" default-label-width=6></my-textbox>
<my-textbox label="LableText6"></my-textbox>
<my-textbox label="LableText7"></my-textbox>
What this is meant to do, is set the label with to 4, for the first 2 instances
then force a width of 5 for the next instance
then go back to 4
then set a new default of 6 for the remaining 3 components.
This is useful in cases where a lot of components (of the same type) are used, most of which are of the same width.
This mechanism will also used for all other applicable attributes.
Please note that what is important here is that the default is set in the parent and can change between instances of the component.
(I am aware that I can have a default value in the template itself but, as I understand it, that would apply to all instances of that component)
Any help would be greatly appreciated!
[Edit]
I have found one solution:
I added these methods to the parent (StoryEditor.vue).
They are called by the component code, shown above with '******' in the comments
<script>
export default {
created: function () {
// make sure the variable exists
if (typeof window.defaultOptions === 'undefined') {
window.defaultOptions = {
defaultLabelWidth: 3,
defaultControlWidth: 7
};
}
},
data() {
return {
story: {
}
}
},
methods: {
getDefaultOptions: () => {
console.log('getDefaultOptions', window.defaultOptions);
},
setDefaultOptions: (opts) => {
window.defaultOptions = opts;
},
getDefault: (option) => {
console.log(' getDefault', window.defaultOptions);
return window.defaultOptions[option];
},
setDefault: (option, v) => {
window.defaultOptions[option] = v;
console.log('setDefault', window.defaultOptions);
}
}
}
</script>
This uses this.$parent. to call methods in the parent.
The parent then uses a window variable to store/retrieve the relevant parameters.
A window variable is used because I want to have a single variable that will be used by all instances of the component.
In VueJS 2 I am trying to create a component that gets and passes data back to the parent which then passes it to another component to display.
The component that gets the data has a user input field it uses to search. When I have it pass data back to the parent using $emit the value in the input keeps being wiped.
I am receiving the below mutation error but I haven't directly tried to change the userSearch field in the component so I am not sure why.
"Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "userSearch" (found in PersonField)"
Relevant html
<person-field v-on:event_child="eventChild"></person-field>
<person-search :prop="personListArray" ></person-search>
Parent app
var app = new Vue({
el: '#app',
data: {
personListArray : [],
tempArray: []
},
methods: {
eventChild: function (arr) {
this.personListArray = arr
}
}
})
Component 1, displays a user input. Uses the input to search and bring back data. Starts search when the length of the input is more then 2. As soon as you hit the 3rd character something is causing the input to clear which I don't want.
Vue.component('person-field', {
props: ['userSearch'],
template: '<input class="form-control" v-model="userSearch" >',
watch: {
userSearch: function () {
var arr = []
if (typeof this.userSearch !== 'undefined') { //added this because once i passed 3 characters in the field the userSearch variable becomes undefined
if (this.userSearch.length > 2) {
$.each(this.getUsers(this.userSearch), function (index, value) {
var obj = {
Title: value.Title,
ID: value.ID
}
arr.push(obj)
});
this.$emit('event_child', arr) //emits the array back to parent "eventChild" method
} else {
console.log('no length')
}
} else {
console.log('cant find field')
}
},
},
methods: {
getUsers: function (filter) {
//gets and returns an array using the filter as a search
return arr
},
}
});
Component 2 - based on the personListArray which is passed as a prop, displays the results as a list (this works)
Vue.component('person-search', {
props: ['prop'],
template: '<ul id="personList">' +
'<personli :ID="person.ID" v-for="person in persons">' +
'<a class="" href="#" v-on:click="fieldManagerTest(person.Title, person.ID)">{{person.Title}}</a>' +
'</personli></ul>',
computed: {
persons: function () {
return this.prop
}
},
methods: {
fieldManagerTest: function (title, ID) { //Remove item from users cart triggered via click of remove item button
//var user = ID + ';#' + title
//this.internalValue = true
//this.$emit('fieldManagerTest');
//this.$parent.$options.methods.selectManager(user)
},
},
});
Component 3, part of component 2
Vue.component('personli', {
props: ['ID'],
template: '<transition name="fade"><li class="moving-item" id="ID"><slot></slot></li></transition>'
})
;
The reason you get the warning,
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"userSearch" (found in PersonField)
Is because of this line
<input class="form-control" v-model="userSearch" >
v-model will attempt to change the value of the expression you've told it to, which in this case is userSearch, which is a property.
Instead, you might copy userSearch into a local variable.
Vue.component('person-field', {
props: ['userSearch'],
data(){
return {
searchValue: this.userSearch
}
},
template: '<input class="form-control" v-model="searchValue" >',
...
})
And modify your watch to use searchValue.
Here is an example.