Versions:
VueJs: 2.2.2
Vee-Validate: 2.0.0-beta.25
Description:
I'm wondering if there's a way to have a unique validator for multiple fields?
Typically, an Address form with 1 input fot the street, 1 for the number and 1 for the city
I want to make a validation on the combination of all the elements.
I've read the documentation but I can't find an exemple that could help me for that.
You could apply a custom validator to a custom component that contained all the fields you want to validate together. For example, you could build a location component (using location instead of address because address is an HTML5 element, and you cannot name a Vue component the same as an existing HTML element).
Vue.component("location", {
props:["value"],
template: "#location-template",
data(){
return {
location: this.value
}
},
methods:{
update(){
this.$emit('input', Object.assign({}, this.location))
}
},
})
Then you can build a validator for that component.
const locationValidator = {
currentLocation: null,
getMessage(field, args) {
if (!this.currentLocation.street)
return "Location requires a street";
if (!this.currentLocation.street_number)
return "Location requires a street_number";
if (!this.currentLocation.city)
return "Location requires a city";
},
validate(location, args) {
this.currentLocation = location;
if (!location.street || !location.street_number || !location.city)
return false;
return true
}
};
Finally, you can pull that together in your Vue.
new Vue({
el:"#app",
data:{
loc: {}
},
created(){
this.$validator.extend("location", locationValidator)
}
})
And your Vue template
<span v-show="errors.has('location')" style="color:red">{{ errors.first('location') }}</span>
<location v-validate="'location'" v-model="loc" data-vv-name="location"></location>
Here is an example.
Related
I am working on extending a Vue.js frontend application. I am currently inspecting a render function within a functional component. After looking over the docs, I had the current understanding that the render function within the functional component will return a single VNode created with CreateElement aka h.
My confusion came when I saw a VNode being returned as an element in an array. I could not find any reference to this syntax in the docs. Does anyone have any insight?
export default {
name: 'OfferModule',
functional: true,
props: {
data: Object,
placementInt: Number,
len: Number
},
render (h, ctx) {
let adunitTheme = []
const isDev = str => (process.env.dev ? str : '')
const i = parseInt(ctx.props.placementInt)
const isDevice = ctx.props.data.Component === 'Device'
const Component = isDevice ? Device : Adunit
/* device helper classes */
const adunitWrapper = ctx.props.data.Decorate?.CodeName === 'AdunitWrapper'
if (!isDevice /* is Adunit */) {
const offerTypeInt = ctx.props.data.OfferType
adunitTheme = [
'adunit-themes',
`adunit-themes--${type}`.toLowerCase(),
`adunit-themes--${theme}`.toLowerCase(),
`adunit-themes--${type}-${theme}`.toLowerCase(),
]
}
const renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate =
ctx.props.data.Decorate?.Position === 'AboveAdunit' || false
const renderOfferModuleWithoutDisplayAdContainers =
ctx.props.data.Decorate?.RemoveAds /* for adunits */ ||
ctx.props.data.DeviceData?.RemoveAds /* for devices */ ||
false
const getStyle = (className) => {
try {
return ctx.parent.$style[className]
} catch (error) {
console.log('$test', 'invalid style not found on parent selector')
}
}
const PrimaryOfferModule = (aboveAdunitSlot = {}) =>
h(Component, {
props: {
data: ctx.props.data,
itemIndex: i,
adunitTheme: adunitTheme.join('.')
},
attrs: {
class: [
...adunitTheme,
getStyle('product')
]
.join(' ')
.trim()
},
scopedSlots: {
...aboveAdunitSlot
}
})
if (renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate) {
return [
PrimaryOfferModule({
aboveAdunit (props) {
return h({
data () {
return ctx.props.data.Decorate
},
template: ctx.props.data.Decorate?.Template.replace(
'v-show="false"',
''
)
})
}
})
]
} else if (renderOfferModuleWithoutDisplayAdContainers) {
return [PrimaryOfferModule()]
} else {
const withAd = i > 0 && i % 1 === 0
const adWrap = (placement, position, className) => {
return h(
'div',
{
class: 'm4d-wrap-sticky'
},
[
h(Advertisement, {
props: {
placement,
position: String(position)
},
class: getStyle(className)
})
]
)
}
return [
withAd && adWrap('inline-sticky', i, 'inlineAd'),
h('div', {
class: 'm4d-wrap-sticky-adjacent'
}),
h(
'div',
{
attrs: {
id: `inline-device--${String(i)}`
},
class: 'inline-device'
},
isDev(`inline-device id#: inline-device--${String(i)}`)
),
withAd &&
i !== ctx.props.len - 1 &&
h(EcomAdvertisement, {
props: {
placement: 'inline-static',
position: String(i)
},
class: getStyle('inlineStaticAd')
}),
PrimaryOfferModule()
]
}
}
}
It turns out that returning an array of VNodes actually predates the scopedSlots update.
I couldn't find it documented anywhere in the docs either, but via this comment on a Vue GitHub issue by a member of the Vue.js core team (which predates the scopedSlots commit by ~1 year), render() can return an Array of VNodes, which Vue will take and render in order. However, this only works in one, singular case: functional components.
Trying to return an array of VNodes with greater than 1 element in a normal (non-functional, stateful) component results in an error:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.
But doing this in a functional component, as your example does, works just fine:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
functional: true, // <--- This is the key
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
If you're interested in the why, another member of the Vue core team explained this limitation further down in the thread.
It basically boils down to assumptions made by the Vue patching and diffing algorithm, with the main one being that "each child component is represented in its parent virtual DOM by a single VNode", which is untrue if multiple root nodes are allowed.
The increase in complexity to allow this would require large changes to that algorithm which is at the very core of Vue. This is a big deal, since this algorithm must not only be good at what it does, but also very, very performant.
Functional components don't need to conform to this restriction, because "they are not represented with a VNode in the parent, since they don't have an instance and don't manage their own virtual DOM"– they're stateless, which makes the restriction unnecessary.
It should be noted, however, that this is possible on non-functional components in Vue 3, as the algorithm in question was reworked to allow it.
It seems this was implemented in:
https://github.com/vuejs/vue/commit/c7c13c2a156269d29fd9c9f8f6a3e53a2f2cac3d
This was a result of an issue raised in 2018 (https://github.com/vuejs/vue/issues/8056) , because this.$scopedSlots.default() returned both a VNode or an array of VNodes depending on the content.
The main argument was that this is inconsistent with how regular slots behave in render functions, and means any render function component rendering scoped slots as children needs to type check the result of invoking the slot to decide if it needs to be wrapped in an array
So Evan comments on the issue thread here, explaining that this.$scopedSlots.default would always return Arrays beginning v2.6 to allow for consistency, but to avoid breaking changes for how $scopedSlots was being used, the update would also allow return of an Array of a single VNode from render functions as well.
I use SVG map of Austria. Each province has "title" attribute which contains a name of the province.
[full example available in the snippet below]
Outside the map in my HTML I have a paragraph, where I want to display the name of the province that was clicked by the user.
{{ province }}
How can I achieve that?
Here is my snippet with my code:
https://mdbootstrap.com/snippets/jquery/marektchas/500840?view=project
If you can change the svg part you can add a click event on each path:
<path #click="setProvince('Burgenland')" id="AT-1" title="Burgenland" class="land" d="..." />
And add a method in your script:
methods: {
setProvince (title) {
this.province = title
}
}
Updated answer
If you have a lot of provinces, you can add the click event on mounted by selecting all the <path> selectors (or class name or any selector you find relevant for your case):
new Vue({
el: '#app',
data: {
province: null
},
mounted () {
this.addClickHandler()
},
methods: {
setProvince (title) {
this.province = title
},
addClickHandler () {
let paths = this.$el.querySelectorAll('path')
paths.forEach(el => {
let title = el.attributes.title.value
el.addEventListener('click', () => {
this.setProvince(title)
})
})
}
}
});
There's no need of #click in template anymore this way.
Live example here
You need to learn about event delegation. You should be able to attach an event handler to the svg root and get events for any element inside the svg
https://recursive.codes/blog/post/34
https://jqfundamentals.com/chapter/events
I am new to Vue.js
While rendering the html, I am invoking a Vue.filter. It should show a date in another format.
Below is my js file :
var details = new Vue({
el: '#ajax-article-detail',
data: {
message: 'Hello Vue.js!'
},
methods: {
showName: function() {
console.log('Calling showName...');
return 'Im Event';
}
}
});
Vue.filter('parseDate', function(date, format) {
if (date) {
//console.log(moment(String(date)).format(format));
return moment(String(date)).format(format);
}
});
and in html, I am calling like {{${start_date} | parseDate('ddd, Do MMM YYYY')}}
and as a response, I am getting same statement.
means, I am getting {{${start_date} | parseDate('ddd, Do MMM YYYY')}} as it is in html.
Can anyone please suggest what I did wrong ?
Thank you.
I have changed your code by adding the filter property within the Vue component creation.
var details = new Vue({
el: '#ajax-article-detail',
data: {
message: 'Hello Vue.js!'
},
methods: {
showName: function() {
console.log('Calling showName...');
return 'Im Event';
}
},
filters: {
parseDate: function(date, format) {
console.log('value passed: ' + date); //check the browser console if it is passed
if (date) {
//console.log(moment(String(date)).format(format));
return moment(String(date)).format(format);
}
}
});
Then use the filter like this-
{{2018-12-19 16:46:00 | parseDate('<your_date_format>') }}
However, you need to check if you are passing the value correctly. Try passing some hard coded string value first and check the console window.
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.