Dynamically adding different components in Vue - javascript

I want to create a simple form builder with Vue where users click on buttons from a menu to add different form fields to a form. I know that if there was just one type of form field to add, I could do it with something like this (https://jsfiddle.net/u6j1uc3u/32/):
<div id="app">
<form-input v-for="field in fields"></form-input>
<button type="button" v-on:click="addFormElement()">Add Form Element</button>
</div>
<script type="x-template" id="form-input">
<div>
<label>Text</label>
<input type="text" />
</div>
</script>
And:
Vue.component('form-input', {
template: '#form-input'
});
new Vue({
el: '#app',
data: {
fields: [],
count: 0
},
methods: {
addFormElement: function() {
this.fields.push({type: 'text', placeholder: 'Textbox ' + (++this.count)});
}
}
})
But what if there's more than one type of form field (input, file, select, etc...)? I was thinking maybe build a different component for each type, but then how would I show multiple types of components in a single list of form elements?
Could I maybe create a component with children components of different types based on the data in the fields array?
Or is there a better way to go about this situation that I'm missing? I've just started learning Vue, so any help is appreciated!

Ok, so I looked into dynamic elements and managed to pull this together:
Vue.component('form-input', {
template: '#form-input'
});
Vue.component('form-select', {
template: '#form-select'
});
Vue.component('form-textarea', {
template: '#form-textarea'
});
new Vue({
el: '#app',
data: {
fields: [],
count: 0
},
methods: {
addFormElement: function(type) {
this.fields.push({
'type': type,
id: this.count++
});
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.min.js"></script>
<div id="app">
<component v-for="field in fields" v-bind:is="field.type" :key="field.id"></component>
<button type="button" v-on:click="addFormElement('form-input')">Add Textbox</button>
<button type="button" v-on:click="addFormElement('form-select')">Add Select</button>
<button type="button" v-on:click="addFormElement('form-textarea')">Add Textarea</button>
</div>
<script type="x-template" id="form-input">
<div>
<label>Text</label>
<input type="text" />
</div>
</script>
<script type="x-template" id="form-select">
<div>
<label>Select</label>
<select>
<option>Option 1</option>
<option>Option 2</option>
</select>
</div>
</script>
<script type="x-template" id="form-textarea">
<div>
<label>Textarea</label>
<textarea></textarea>
</div>
</script>
So instead of creating a new form-input component for each item in the fields array, I'm creating a new component that is associated with the correct component via the type property of the fields

You can pass the field object as props of your form-input component and make the type dynamic:
Vue.component('form-input', {
template: '#form-input',
props: ['field']
})
new Vue({
el: '#app',
data: {
fields: [],
inputType: '',
count: 0
},
methods: {
addFormElement(val) {
this.fields.push({type: val, placeholder: 'Textbox ' + (++this.count)});
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<h3>Add form element</h3>
<select size="3" v-model='inputType' #click="addFormElement(inputType)">
<option value="text">Text</option>
<option value="checkbox">Checkbox</option>
<option value="radio">Radio</option>
</select>
<p>
<form-input v-for="field in fields" :field="field"></form-input>
</p>
</div>
<template id="form-input">
<div>
<label>{{ field.type }}</label>
<input :type="field.type" />
</div>
</template>

Based on the code from the answer, one could add dynamic content for each one of those form controls as well ( the full concept could be seen from the following site):
Vue.component('form-input', {
template: '#form-input'
, props: ['label','cnt']
});
Vue.component('form-select', {
template: '#form-select'
, props: ['label','cnt']
});
Vue.component('form-textarea', {
template: '#form-textarea'
, props: ['label','cnt']
});
new Vue({
el: '#app',
data: {
fields: [],
count: 0
}
, mounted() {
// fetch those from back-end
this.addFormElement('form-input','lbl', "form-input-content")
this.addFormElement('form-textarea','lbl', "form-textarea-content")
var select_cnt = [
{'value': 1, 'text': 'item-01'},
{'value': 2, 'text': 'item-02'}
]
this.addFormElement('form-select','some-label',select_cnt)
}
, methods: {
addFormElement: function(type,label,cnt) {
this.fields.push({
'type': type
, id: this.count++
, 'label':label
, 'cnt':cnt
});
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.min.js"></script>
<div id="app">
<component v-for="field in fields" v-bind:is="field.type" :key="field.id" :cnt="field.cnt" :label="field.label"></component>
</div>
<script type="x-template" id="form-input">
<div v-on:keyup.tab="this.document.execCommand('selectAll',false,null);">
<label>{{label}}</label>
<input type="text" :value="cnt"/>
</div>
</script>
<script type="x-template" id="form-textarea">
<div v-on:keyup.tab="this.document.execCommand('selectAll',false,null);">
<label>{{label}}</label>
<textarea :value="cnt"></textarea>
</div>
</script>
<script type="x-template" id="form-select">
<div>
<label>Select</label>
<select>
<option v-for="oitem in cnt" :value="oitem.value">{{oitem.text}}</option>
</select>
</div>
<div v-html="cnt"></div>
</script>

Related

Render nested array objects of unknown depth VUE

How can I render and show nested elements on the UI (from nested value[] field of object) which are generated dynamically when you select option (e.g. List) in parent element?
For example, I got this code below. When you create field and chose List option, you should see one more nested and so on, depth is unknown. How can I render this to show to user? Kind of v-for inside v-for doesnt seem to work. Maybe I need recursion here, but I dont know how to release it.
I appreciate any help!
var app = new Vue({
el: '.container',
data: {
modelname: '',
fields: []
},
methods: {
addNewField() {
this.fields.push({
left: 0,
type:'',
method:'',
size:'',
value:''}
)
},
createChildElement(field) {
if (field.type == "List") {
Vue.set(field, "value", []);
field.value.push({
type:'',
left: field.left+20,
method:'',
size:'',
value:''}
);
}
},
showJson() {
const data = this.fields
alert(JSON.stringify(data, null, 2));
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div class="container">
<h1 class="font-italic text-warning">TDGT Web Client</h1>
<!--<button id="add-model" class="btn btn-warning" #click="addNewModel">Создать модель</button>-->
<div>
<button id="add-field" class="btn btn-sm btn-warning" #click="addNewField">Create field</button>
<button id="show-json" class="btn btn-sm btn-warning" #click="showJson">Show JSON</button>
<div v-for="(field, index) in fields" :style="{marginLeft: field.left+'px'}" :key="index">
<ul>
<li>
<select v-model="field.type" v-on:change="createChildElement(field)" aria-label="Выбрать тип поля">
<option selected>Тип поля</option>
<option value="List">List</option>
<option value="Map">Map</option>
<option value="Integer">Integer</option>
</select>
<select v-model="field.method" v-if="field.type === 'Map' || field.type === 'List'" aria-label="Метод генерации">
<option selected>Тип значения</option>
<option value="Static">Static</option>
<option value="Random">Random</option>
<option value="Range">Range</option>
</select>
<input type="text" v-if="field.type === 'Map' || field.type === 'List'" v-model="field.size" placeholder="Размерность">
<input type="text" v-if="field.type === 'Integer'" v-model="field.value" placeholder="Значение">
</li>
<ul></ul>
</ul>
</div>
</div>
</div>
UPD. Based on answers I tried to make a solution for my task but I still have some problems. I moved most of the code to component, but I receive a lot of errors which I cant resolve. e.g.:
Invalid prop: type check failed for prop "item". Expected Object, got Array.
Property or method "fields" is not defined on the instance but referenced during render.
Here is my code:
Vue.component("el-inpt-group", {
template: "#item-template",
props: {
item: Object,
}
});
var app = new Vue({
el: '.container',
data: {
modelname: '',
fields: [
]
},
methods: {
addNewField() {
this.fields.push({
name: '',
left: 0,
type:'',
method:'',
size:'',
value:''}
)
},
createChildElement(field) {
if (field.type == "List") {
Vue.set(field, "value", []);
field.value.push({
name: '',
type:'',
left: field.left+20,
method:'',
size:'',
value:''}
)
}
},
showJson() {
const data = this.fields
alert(JSON.stringify(data, null, 2));
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script type="text/x-template" id="item-template">
<ul>
<li v-for="field in fields">
<input type="text" v-model="field.name" placeholder="Name">
<select v-model="field.type" v-on:change="createChildElement(field)" aria-label="Выбрать тип поля">
<option selected>Тип поля</option>
<option value="List">List</option>
<option value="Map">Map</option>
<option value="Integer">Integer</option>
</select>
<select v-model="field.method" v-if="field.type === 'Map' || field.type === 'List'" aria-label="Метод генерации">
<option selected>Тип значения</option>
<option value="Static">Static</option>
<option value="Random">Random</option>
<option value="Range">Range</option>
</select>
<input type="text" v-if="field.type === 'Map' || field.type === 'List'" v-model="field.size" placeholder="Размерность">
<input type="text" v-if="field.type === 'Integer'" v-model="field.value" placeholder="Значение">
</li>
<ul><el-inpt-group v-for="child in fields.value" :style="{marginLeft: field.left+'px'}" :key="child.name" :item="child"></el-inpt-group></ul>
</ul>
</script>
</head>
<body>
<div class="container">
<h1 class="font-italic text-warning">TDGT Web Client</h1>
<button id="add-field" class="btn btn-sm btn-warning" #click="addNewField">Create field</button>
<button id="show-json" class="btn btn-sm btn-warning" #click="showJson">Show JSON</button>
<el-inpt-group :item="fields"></el-inpt-group>
</div>
</div>
Indeed recursion is your solution here.
When you can't see how to organise your code properly, often that means you haven't divided your code into enough components.
To make a simple example, just create a component that takes one item as a property, and then call itself when this item as sub items.
Given this example structure:
[
{
name: 'Todo 1'
},
{
name: 'Todo 2',
items: [
{
name: 'Todo 2.1'
},
{
name: 'Todo 2.2'
}
],
},
{
name: 'Todo 3',
items: []
},
]
<template>
<div>
<article v-for="subTodos of todo.items" :key="subTodos.name">
<h1>{{ subTodos.name }}</h1>
<Todo :todo="subTodos" />
</article>
</div>
</template>
<script>
export default {
name: 'Todo',
props: {
todo: { type: Object, required: true }
}
}
</script>
<template>
<Todo :todo="firstItem" />
</template>
<script>
export default {
data () {
return {
firstItem: { name: 'First todo', items: nestedTodos }
}
}
}
</script>
Here, as long as todo.items isn't an empty array, it will create a <Todo> component, which himself creates more <Todo> elements whenever they have items themselves...

Based on v-on:click, How to uncheck the checkbox values in Vuejs?

<div class="reser-productlist">RESET</div>
<div class="checkbox-alignment-form-filter">
<input type="checkbox" id="Coils" value="Coils" class="vh-product" v-model="checkedNames" />
<label class="productlist-specific" for="Coils">Cs</label>
</div>
<div class="checkbox-alignment-form-filter">
<input type="checkbox" id="Sheets" value="Sheets" class="vh-product" v-model="checkedNames" />
<label class="productlist-specific" for="Sheets">Sts</label>
</div>
Based on v-on:click, How to uncheck/reset the checkbox,
Basically i want to uncheck the checkboxes, When user click on the RESET button/label. Generally to do this do i need to use the v-model or id or for in label to target and trigger the reset functionality?
I think you can use below code.
<div class="reser-productlist" #click="reset">RESET</div>
<script>
export default
{
methods:{
reset()
{
this.checkedNames = '';
}
}
}
</script>
const App = new Vue({
el: '#app',
template: `<div>
<div class="reser-productlist" #click="reset">RESET</div>
<div v-for="product in products" class="checkbox-alignment-form-filter">
<input type="checkbox" :id="product" :value="product" class="vh-product" v-model="checkedNames" />
<label class="productlist-specific" :for="product">{{ product }}</label>
</div>
</div></div>
`,
data() {
return {
products: ['Coils', 'Sheets'],
checkedNames: [],
}
},
methods: {
reset() {
this.checkedNames = []
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<div id=app>

VueJS = Uncaught TypeError: Cannot read property 'settings' of undefined

I have a basic input that should update a data on input change, but it doesn't because it returns Uncaught TypeError: Cannot read property 'settings' of undefined
Vue component
<template>
<div>
<div class="inner_container">
<p>
URL:
<span>{{settings.url}}</span>
</p>
<div class="input_row">
<input
class="input_text"
id="getURL"
type="url"
inputmode="url"
placeholder="Enter URL"
title="URL"
#input="changeUrl"
/>
<label class="label_" for="getURL">URL Link</label>
</div>
<button class="button" #click="saveSettings">Save all Values</button>
</div>
<div>
</template>
<script>
import axios from "axios";
import _ from "lodash";
export default {
name: "Home",
data() {
return {
settings: {
brightness: "",
},
};
},
methods: {
changeUrl: _.debounce((e) => {
this.settings.url = e.target.value;
}, 500),
},
};
</script>
On each input change I receive the above error.
What am I doing wrong ?
The problem is that this in the arrow function is not referring to the object you want. One solution is to use a normal function and predefine the property url in settings:
new Vue({
el: '#app',
data() {
return {
settings: {
brightness: "",
url: ""
},
};
},
methods: {
changeUrl: _.debounce(function(e) {
this.settings.url = e.target.value;
}, 500),
saveSettings(){
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.min.js"></script>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app">
<div class="inner_container">
<p>
URL: <span>{{settings.url}}</span>
</p>
<div class="input_row">
<input
class="input_text"
id="getURL"
type="url"
inputmode="url"
placeholder="Enter URL"
title="URL"
#input="changeUrl"
/>
<label class="label_" for="getURL">URL Link</label>
</div>
<button class="button" #click="saveSettings">Save all Values</button>
</div>
</div>
A simpler approach you may find useful is to set the variable using v-model:
new Vue({
el: '#app',
data() {
return {
settings: {
brightness: "",
},
};
},
methods: {
saveSettings(){
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.min.js"></script>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app">
<div class="inner_container">
<p>
URL: <span>{{settings.url}}</span>
</p>
<div class="input_row">
<input
class="input_text"
id="getURL"
type="url"
inputmode="url"
placeholder="Enter URL"
title="URL"
v-model="settings.url"
/>
<label class="label_" for="getURL">URL Link</label>
</div>
<button class="button" #click="saveSettings">Save all Values</button>
</div>
</div>
Ciao, try to use debounce like this:
_.debounce(function(e) {
this.settings.url = e.target.value;
}, 500)
The problem is that you are using this in a statement that is not being called on a JS class. Fix this by changing the this to a variable name, and setting that variable name to a class.

How to enable / disable an input with a select VueJS

Question originally posted (in Spanish) on es.stackoverflow.com by José:
I have seen the example in JavaScript, and yes, it works and
everything but as I can do it in vue.js, I've been trying for a
while and it does not work. Sorry for the inconvenience.
<script src="https://unpkg.com/vue"></script>
<div id="app">
<p>{{ name }}</p>
<input type="text" v-model="name"/>
<button type="submit" :disabled="name == defaultName">Submit</button>
</div>
>
Javascript
$(document).ready(function() {
$('#id_categoria').change(function(e) {
if ($(this).val() === "1") {
$('#d').prop("disabled", true);
} else {
$('#d').prop("disabled", false);
}
})
});
HTML
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<div>
<select name='id_categoria' id='id_categoria' >
<option value="1" selected>Clientes</option>
<option value="2">Empresas</option>
<option value="3">Personas</option>
</select>
<input id="d" disabled="disabled" type="text" value="test">
</div>
I know it's weird that I want to directly manipulate the DOM when you use Vue; I usually let the framework do that, This would be a possible solution
Of course, if it's a possibilty, you should take advantage of Vue's data-driven reactive nature, as tackling with the DOM is always tricky.
The solution would be to just create another variable and populate it on
new Vue({
el: '#app',
data: {
name: 'Hello Vue.js!',
defaultName: null
},
mounted() {
this.defaultName = this.name;
}
Below is a vue template file using v-model instead of combining jQuery
<template>
<div>
<select v-model="id_categoria">
<option value="1" :selected="id_categoria === 1">Clientes</option>
<option value="2" :selected="id_categoria === 2">Empresas</option>
<option value="3" :selected="id_categoria === 3">Personas</option>
</select>
<input id="d" :disabled="id_categoria === 1" type="text" value="test">
</div>
</template>
<script>
export default {
data() {
return {
id_categoria: 1,
};
},
}
</script>
Try this:
new Vue({
el: "#app",
data: {
options: [
{ content: 'Clientes', value: 1 },
{ content: 'Empresas', value: 2 },
{ content: 'Personas', value: 3 }
],
inputDisabled: true
},
methods: {
onChangeSelect(e) {
this.inputDisabled = (e.target.value == 1)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<select #change="onChangeSelect($event)">
<option v-for="(option, index) in options" :key="index" :value="option.value">
{{ option.content }}
</option>
</select>
<input :disabled="inputDisabled" type="text" value="test">
</div>
new Vue({
el: "#app",
data: {
options: [
{ content: 'Clientes', value: 1 },
{ content: 'Empresas', value: 2 },
{ content: 'Personas', value: 3}
],
selectedOption: '',
},
computed: {
inputDisabled() {
return this.selectedOption === 2;
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<select v-model="selectedOption">
<option v-for="(option, index) in options" :key="index" :value="option.value">{{ option.content }}</option>
</select>
<input :disabled="inputDisabled" type="text" v-model="selectedOption">
</div>

How to adjust text field reactivity in Vue.js

I have a simple text field in Vue.js:
new Vue({
el: '#app',
data: {
value: 'Test'
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="value" ref="input" />
<p>{{ value }}</p>
</div>
Is it possible to decrease the frequency of updating the value in the data model from 'onChange' to 'onBlur'?
v-model is just syntax sugar for =>
:value="modelValue" #input="modelValue = $event.target.value"
If you want something else, it's very easy to do. Just change the update side to onBlur, so =>
<input class="form-control
:value="value"
#blur="value = $event.target.value"
#input="value = $event.target.value"
>
The improved example code:
new Vue({
el: '#app',
data: {
value: 'Test'
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input
type="text"
:value="value"
#blur="value = $event.target.value"
ref="input"
/>
<p>{{ value }}</p>
</div>
Yes, you should Just add #blur event, and pass through it the value of the event
Then when this event gets triggered in methods, it will change the value of result to the input value...so the updating became only conditioned to blur of the input
new Vue({
el: '#app',
data: {
result: '',
value:''
},
methods:{
blo(e){
this.result = e
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div id="app">
<input type="text" #blur='blo(value)' v-model="value" />
<p>{{ result }}</p>
</div>
</div>

Categories