I've done a couple of projects with React in the last year and I've switched to Vue for my current one, attracted by its greater simplicity, less verbose nature and the fact that you don't have to transpile your code to work, so it's easier to get going with and more flexible (well, to be accurate you don't have to transpile with React either, there's no need to use JSX, but it loses one of its great benefits if you don't).
Anyway, one of the things I'm missing from React (and I'm sure it's just ignorance of the Vue way which is my problem) is a way of reusing a fragment of code to avoid repeating myself in templates. The specific situation which prompted this question was a template where I have a custom input element like this:
<input ref="input" :id='name' :name='name' :type='fieldType' class='form-control' :value="value" :readonly="readonly" :disabled="disabled" #input="handleInput"/>
In certain situations I'd want to wrap it in a div, otherwise I'd want to use it as is. With React, I'd simply store it in a variable, something like this:
var inp=( <input ref="input" :id='name' :name='name' :type='fieldType' class='form-control' :value="value" :readonly="readonly" :disabled="disabled"
#input="handleInput"/>);
Then I could do something like the following:
var myInput;
if(divSituation){
myInput=(<div>{inp}</div>);
} else {
myInput=inp;
}
Then I could use the myInput var. The Vue logic doesn't seem to allow this, though. Unless, of course, using JSX within Vue would allow me to do the very same thing? I currently have for this in Vue something like the following, which offends me:
<template v-if="divSituation">
<div><input ref="input" :id='name' :name='name' :type='fieldType' class='form-control' :value="value" :readonly="readonly" :disabled="disabled" #input="handleInput"/></div>
</template>
<template v-else>
<input ref="input" :id='name' :name='name' :type='fieldType' class='form-control' :value="value" :readonly="readonly" :disabled="disabled" #input="handleInput"/
</template>
You can create vue components for re-usable components, which can be used as par requirement.
You can find an example of re-usable input component in vue docs:
<currency-input v-model="price"></currency-input>
and you can write that as re-usable component like following:
Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)"\
>\
</span>\
',
props: ['value'],
methods: {
// Instead of updating the value directly, this
// method is used to format and place constraints
// on the input's value
updateValue: function (value) {
var formattedValue = value
// Remove whitespace on either side
.trim()
// Shorten to 2 decimal places
.slice(0, value.indexOf('.') + 3)
// If the value was not already normalized,
// manually override it to conform
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// Emit the number value through the input event
this.$emit('input', Number(formattedValue))
}
}
})
You can add more props for readonly, disabled, etc.
You can also have a look at custom input elements of element-ui and it's code.
Given the example you have given, You can use v-html more efficiently. with v-html, you can pass a HTML string which will be rendered as HTML. However Note: the contents are inserted as plain HTML - they will not be compiled as Vue templates.
You can have a computed property, which will return HTML string as par your variable: divSituation, like following:
var data = {
templateInput: '<input ref="input" :id="name" :name="name" :type="fieldType" class="form-control" :value="value" :readonly="readonly" :disabled="disabled" #input="handleInput"/>',
divSituation: true,
myInput: ''
}
var demo = new Vue({
el: '#demo',
data: function(){
return data
},
computed: {
getMyInput: function(){
if(this.divSituation){
return this.templateInput
}
else{
return '<div>' + this.templateInput + '</div>'
}
}
}
})
Now you can just render myInput in HTML using v-html like this:
<div id="demo">
<div v-html="getMyInput">
</div>
</div>
check out working fiddle.
Related
I created a custom Input component.
I needed pass #blur from vee-validate form to my custom input component.
It works great in normal html input tag. I no idea how could we pass the #blur into custom Input component.
Example 1 work correctly, it triggered the validation after blur the input.
<template>
<form #submit="onSubmit">
<input #blur="emailBlur" v-model="email" type="text" autocomplete="off" name="email" placeholder="email">
<button type="submit" :disabled="isSubmitting">Submit</button>
</form>
</template>
Example 2 with My custom Input Component:
// src/components/Input.vue
<template>
<div class="mt-2">
<label :for="name" class="h5">Name</label>
<input
:type="type"
:value="modelValue"
#change="$emit('update:modelValue', $event.target.value)"
:id="name"
:placeholder="placeholder"
/>
</div>
</template>
<script>
export default {
name: 'Input',
props: ["modelValue", 'name', 'type', 'placeholder'],
setup(props) {
console.log('props :>> ', props); // not receive the #blur
}
}
</script>
Parent Component (App.vue):
<template>
<form>
<Input #blur="emailBlur" v-model="email" type="text" name="email" placeholder="Custom input email" />
<button type="submit" :disabled="isSubmitting">Submit</button>
</form>
</template>
export default {
name: 'App',
components: {
Input
},
setup() {
// the vee validation values v-model to template
}
}
Sending the handler as props from the parent is not a good practice instead need to trigger the handler(present in the parent) from the child component. By doing so you will have an advantage where you can bind different blur handlers based on your requirement inside different parent components
To do so you can follow the below approach
Custom Input Component
// src/components/Input.vue
<template>
<div class="mt-2">
<label :for="name" class="h5">Name</label>
<input
:type="type"
:value="modelValue"
#change="$emit('update:modelValue', $event.target.value)"
:id="name"
:placeholder="placeholder"
#blur="$emit('blur')" //Change added
/>
</div>
</template>
<script>
export default {
name: 'Input',
props: ["modelValue", 'name', 'type', 'placeholder'],
emits: ['blur', 'update:modelValue'], // change added
}
</script>
Note:
for all v-models without arguments, make sure to change props and events name to modelValue and update:modelValue respectively
For Example:
Parent.vue
<ChildComponent v-model="pageTitle" />
and in Child.vue it should be like
export default {
props: {
modelValue: String // previously was `value: String`
},
emits: ['update:modelValue'],
methods: {
changePageTitle(title) {
this.$emit('update:modelValue', title) // previously was `this.$emit('input', title)`
}
}
}
What you are creating is usually called "transparent wrapper" component. What you want from this wrapper is to behave in almost every way as normal input component so the users of the component can work with it as it was normal input (but it is not)
In your case, you want to attach #blur event listener to your wrapper. But the problem is that blur is native browser event i.e. not a Vue event.
When you place event listener on a component for an event not specified in emits option (or v-bind an attribute that is not specified in component's props), Vue will treat it as Non-Prop Attribute. This means it take all such event listeners and non-prop attributes and place it on the root node of the component (div in your case)
But luckily there is a way to tell Vue "Hey, don't place those automatically on root, I know where to put it"
Use inheritAttrs: false option on your component
Put all non-prop attributes (including the event listeners) on the element you want - input in this case - using v-bind="$attrs"
Now you can even remove some props - for example placeholder (if you want), because if you use it directly on your component, Vue place it on input, which is what you want...
Also handling #change event is not optimal - you component allows to specify a type and different input types has different events. Nice trick around it is not to pass value and bind event explicitly, but instead use v-model with computed (see example below)
const app = Vue.createApp({
data() {
return {
text: ""
}
},
methods: {
onBlur() {
console.log("Blur!")
}
}
})
app.component('custom-input', {
inheritAttrs: false,
props: ["modelValue", 'name', 'type'],
emits: ['update:modelValue'],
computed: {
model: {
get() { return this.modelValue },
set(newValue) { this.$emit('update:modelValue', newValue) }
}
},
template: `
<div class="mt-2">
<label :for="name" class="h5">{{ name }}:</label>
<input
:type="type"
v-model="model"
:id="name"
v-bind="$attrs"
/>
</div>
`
})
app.mount("#app")
<script src="https://unpkg.com/vue#3.2.19/dist/vue.global.js"></script>
<div id='app'>
<custom-input type="text" name="email" v-model="text" placeholder="Type something..." #blur="onBlur"></custom-input>
<pre>{{ text }}</pre>
</div>
The Vue.js ecosystem comes with some handy modifiers for use with form inputs.
<input v-model.lazy="msg">
<input v-model.trim="msg">
<input v-model.number="msg">
I was wondering if it were possible to chain such modifiers, perhaps something like this:
<input v-model.lazy.trim="msg">
If not, has anyone any experience of making their own modifiers?
Unfortunately it doesn't look like you can make custom modifiers the moment, there's some desire for the feature here. Your best option IMO is to make a custom component and then emit the modified value based on what you need.
I made a quick app (included below) to test chaining modifiers and it does work.
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
},
computed: {
len: function() {
return this.message.length;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model.lazy.trim="message" />
<span style="background:yellow">{{ message }}</span>
<span>{{len}}</span>
</div>
I'm trying to make in a bootstrap-vue table a slot to render any boolean value with a custom component.
So I have a simple table
<b-table :items="items" :fields="columns" >
</b-table>
Now if i want to render a single column in a particular way i have to use a slot
<template v-slot:cell(active)="data" >
<my-component :item="data.item" />
</template>
And it works, because I know that active is a boolean.
I would like to generalize this behavior but i cannot use v-for in templates and cannot use v-slot:cell(active) if not on template... The idea was to create an array with all my boolean fields and iterate on it... but it does not work..
Something like this
<template v-slot:cell(b)="data" v-for="b in booleanFields">
<my-component :item="data.item[b]" />
</template>
Because Vue supports Dynamic Slot Names, you can use variables to set the slot names using the v-bind:[attributeName]="value" syntax.
This way you could do something like:
<template v-slot:['cell(' + b + ')']="data" v-for="b in booleanFields">
But using the quotes there is not possible due to the dynamic argument expression constraints. So you'll have to create a helper method to do that concatenation. So:
<template v-slot:[gomycell(b)]="data" v-for="b in booleanFields">
plus
methods: {
gomycell(key) {
return `cell(${key})`; // simple string interpolation
}
Naturally, you could just name the method gomycell as cell and use it like v-slot:[cell(b)]="data" (notice the []s), but I left the name gomycell just so in this texample it is clearer what is the name of the method and what is not.
Demo:
Here's a small demo showcasing the dynamic slot names usage, it's not b-table but I think it is good enough to show it is possible:
Vue.component('my-table', {
template: '#my-table',
})
new Vue({
el: '#app',
data: {
booleanFields: [true, false]
},
methods: {
gomycell(key) {
return `cell(${key})`;
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<my-table>
<template v-slot:[gomycell(b)]="data" v-for="b in booleanFields">
<h3>who? {{ data.is }}</h3>
</template>
</my-table>
</div>
<template id="my-table">
<div>
<div style="color:green"><slot name="cell(true)" v-bind="{is: 'true!'}"></slot></div>
<div style="color:red"><slot name="cell(false)" v-bind="{is: 'false!'}"></slot></div>
</div>
</template>
I currently have a custom vue-form component:
In my common HTML it kind of looks something like this:
<vue-form>
<input type="text" v-model="test">
<input type="password" v-model="test2">
</vue-form>
Note that this is not a Vue template.
My v-model keeps pointing to my root component when in this case I would like my v-model to point the the actual component that is wrapping my content.
For the vue-form component I am simply using a slot like so:
<template>
<div>
<slot></slot>
</div>
</template>
Is there a way to get the v-model binding to point towards the wrapping component instead of the root element?
You can use Scoped Slots documentation.
Edit:
Here is working example:
<div id="app">
<vue-form v-bind:model="x">
<template scope="props">
<input type="text" v-model="props.model.test">
<input type="password" v-model="props.model.test2">
</template>
</vue-form>
</div>
new Vue({
el: '#app',
data: function() {
return {
x: {
test: 'John',
test2: 'Smith'
}
}
},
components: {
'vue-form': {
template: `<div><slot :model="model"></slot></div>`,
props: ['model']
}
}});
jsfiddle
Just notice that this solution requires from you to propagate property from component to slot by :model="model" part in slot declaration.
Is it possible to use ref with el-input component from Element-UI? I am trying to use $refsto focus on the input when my Vue instance is mounted. Here is my code:
<div id="app">
<el-input type="text" ref="test" placeholder="enter text"></el-input>
</div>
And in my Vue instance:
new Vue({
el: "#app",
mounted(){
this.$refs.test.focus()
}
})
The focus method is not working at all, even if I move this.$refs.test.focus() into a method and try to trigger it through an event.
The $refs object stores Vue components and should be working fine. The problem is that you are attempting to invoke a focus method which doesn't exist in the component, but rather on an input somewhere inside the component's template.
So, to actually find the input you'd have to do something like this:
this.$refs.test.$el.getElementsByTagName('input')[0].focus();
Not the prettiest line of code ever made, right? So, instead of calling anything in your app's mounted method, if you want to autofocus on the input, just do this:
<el-input type="text" placeholder="enter text" autofocus></el-input>
This can be also solved your problem.
// Focus the component, but we have to wait
// so that it will be showing first.
this.$nextTick(() => {
this.$refs.inputText.focus();
});
Now you can use it like this
<div id="app">
<el-input type="text" ref="test" placeholder="enter text"></el-input>
</div>
this.$refs.test.focus();
https://element.eleme.io/#/en-US/component/input#input-methods