I am trying to dynamically add components to my vue template. Things have not gone as expected.
I have this component:
Vue.component('form-x-text', {
delimiters: ["[[", "]]"],
template: `
<div v-if="_.indexOf(['text','email','tel','number','url','color'], fType) > 0" class="form-group" :class="{'has-error': errors.has(fName)}">
<label :for="fId">[[fLabel]]</label>
<input v-validate="{ rules: parsedRule }" class="form-control" :type="fType" :name="fName" :id="fId" data-vv-delay="700">
<span class="field-error" v-show="errors.has(fName)"> [[ errors.first(fName) ]]</span>
</div>
`,
props: {
fType: {
type: String,
validation: function(value){
return _.indexOf(['text','email','tel','number','url','color'], value) > 0
},
required: true
},
fName: {
type: String,
required: true
},
fId: null,
fLabel: null,
fRule: null
},
computed: {
parsedRule: function(){
console.log(this.fRule);
return JSON.parse(this.fRule);
}
}
})
class FormX{
constructor(){
}
static newForm(){
return JSON.parse(`
{
"node": "root",
"child": []
}
`)
}
static textInput(ftype, name, label, id, required = false){
let emailR = false;
if (ftype == 'email'){
emailR = true;
}
return JSON.parse(
`
{
"node": "element",
"tag": "form-x-text",
"attr": {
"f-type": "${ftype}",
"f-name": "${name}",
"f-label": "${label}",
"f-id": "${id}"
}
}
`
)
}
}
var builder = new Vue({
el: '#builder',
delimiters: ["[[", "]]"],
data: {
blankTemplate: {
node: 'root',
child: [
{
node: 'element',
tag: 'div',
attr: { id: '1', class: 'foo'},
child: []
},
workingTemplate: {
content: {}
},
primaryButton: {},
secondaryButton: {},
bForm: null,
},
methods: {
openFormDesigner: function(){
$('#formDesignerModal').modal('show');
this.primaryButton = {'show':false, 'text':'Save', 'spanstyle':'fa fa-floppy-o'};
this.secondaryButton = {'show':true, 'text': 'Close'};
},
addToFormCanvas: function(ftype){
if (!ftype){
console.log("You need to provide the type of the Form element");
}else if (ftype == 'email'){
this.bForm.child.push(FormX.textInput('email', 'mail1','mail1','mail1'));
}else if (ftype == 'text'){
this.bForm.child.push(FormX.textInput('text', 'mail1','mail1','mail1'));
}else if (ftype == 'number'){
this.bForm.child.push(FormX.textInput('number', 'mail1','mail1','mail1'));
}
},
jsonToHtml: function(jsono){
return json2html(jsono);
},
getAttr: function(aobj){
return aobj
}
},
mounted: function(){
$('#vwr-panel > iframe').contents().find('body').css({"margin":"0 auto", "background-color":"#fff"}).html(
json2html(this.blankTemplate));
},
created: function(){
this.bForm = FormX.newForm();
},
computed: {
bFormHtml: function(){
return json2html(this.bForm)
}
},
filters: {
capitalize: function(v){
return _.toUpper(v);
}
}
})
When a click happens, I basically append to a data object in my vue instance.
I then use json2html to convert this data object to "html" which is really a component.
I expect that the component will be rendered, but although I can see it in inspector, it doesn't render.
I am inserting the component with <div v-html="bFormHtml"></div>
Have you tried using a mutable <component> tag?
html:
...
<component :is="componentId"></component>
...
javascript:
...
data: {
componentId: 'blank'
},
components: {
'blank': {
'template': '<div></div>'
}
},
methods: {
'updateComponent': function() {
this.componentId = 'form-x-text'
}
}
...
JsFiddle an Example of multiple components.
I'm not sure if it will work exactly with what you have but it might do better than trying to inject the HTML.
Related
I have set a watcher to check if the dropdown value is equals to "select", if the value is equal then a new array "option" is created with one element in it. I have a button that will add a value to the array that the watcher created when the dropdown value is equal to select. The button function is working but the input field that should loop the array doesn't re-render after the button click.
I am sorry I am terrible at explaining things.
here is my code
<template lang="pug">
.formHolder
.quest
input(type="text" placeholder="Question" v-model="tempForm.tempQuestions[0].question")
input(type="text" placeholder="Description" v-model="tempForm.tempQuestions[0].description")
select(name="category" v-model="tempForm.tempQuestions[0].type")
option(value="text") text
option(value="select") Dropdown
option(value="number") Number
this is the input field that will loop the array
.option(v-for="(option, index) in tempForm.tempQuestions[0].options")
input(type="text" placeholder="Option" v-model="tempForm.tempQuestions[0].options[index]")
this button adds a value to the array
button(v-if="tempForm.tempQuestions[0].type === 'select'" #click="addItems") Add Options
</template>
<script>
// # is an alias to /src
import HelloWorld from "#/components/HelloWorld.vue";
export default {
name: "HomeView",
components: {
HelloWorld,
},
data() {
return {
questions: 0,
tempForm: {
tempQuestions: [
{
question: null,
description: null,
type: null,
},
],
},
form: {
questions: [],
},
};
},
methods: {
addQuestion() {
this.tempForm.tempQuestions.push({
question: null,
description: null,
type: null,
});
this.questions++;
this.readerNos++;
},
addOptionArray() {
this.tempForm.tempQuestions[0].options.push("");
},
//////// this is the function
addItems() {
console.log(this.tempForm.tempQuestions[0].options);
let length = this.tempForm.tempQuestions[0].options.length;
console.log(length);
this.$set(this.tempForm.tempQuestions[0].options, length, "");
},
},
watch: {
tempForm: {
handler: function () {
this.tempForm.tempQuestions.forEach((question) => {
if (question.type === "select") {
question.options = [];
question.options.push("");
} else if (question.type === "text") {
if (question.options) {
delete question.options;
}
} else if (question.type === "number") {
if (question.options) {
delete question.options;
}
}
});
},
deep: true,
},
},
};
</script>
You should use Vue.$set() when adding an empty options array - otherwise it won't be reactive and there will be no rerendering of the options in the template:
watch:
{
tempForm:
{
handler()
{
this.tempForm.tempQuestions.forEach((question) =>
{
if (question.type === "select")
{
this.$set(question, 'options', []);
question.options.push("");
}
else if (question.type === "text")
{
if (question.options)
{
question.options.splice();
}
}
else if (question.type === "number")
{
if (question.options)
{
question.options.splice();
}
}
});
},
deep: true,
},
},
I've tried many different ways to do this with both alert() and simple if-else statements, and even what is at this link: https://v2.vuejs.org/v2/cookbook/form-validation.html
But nothing works! What am I doing wrong?
What is in my index.html:
<div id="app">
<div class = "addTask">
<h1>A List of Tasks</h1>
<p v-show="activeItems.length === 0">You are done with all your tasks! Celebrate!</p>
<form #submit.prevent="addItem">
<input type="text" v-model="title">
<button type="submit">+</button>
</form>
</div>
This is what I have in scripts.js:
var app = new Vue({
el: '#app',
data () {
return {
// errors: [],
items: [{
userId: 0,
id: 0,
title: "",
completed: false,
}],
title: '',
show: 'all',
}
},
// Using axios to asynchrously query the API
mounted () {
axios
.get("https://jsonplaceholder.typicode.com/todos")
.then(response => this.items = response.data)
},
computed: {
activeItems() {
this.saveItems;
return this.items.filter(item => {
return !item.completed;
});
},
filteredItems() {
// An active state denotes the task is not yet completed
if (this.show === 'active')
return this.items.filter(item => {
return !item.completed;
});
if (this.show === 'completed')
return this.items.filter(item => {
return item.completed;
});
// This makes it so added tasks go to the top, not bottom
return this.items.reverse();
},
},
methods: {
addItem() {
if(this.items != 0) {
this.items.push({
title: this.title,
completed: false
})
this.title = "";
}
else {
alert("Please enter a task.")
}
Based on your code, you should declare a property called title in the data function and check if title is empty or not in addItem method.
var app = new Vue({
el: '#app',
data () {
return {
// errors: [],
title: '', // add this property in data function
items: [{
userId: 0,
id: 0,
title: "",
completed: false,
}],
title: '',
show: 'all',
}
},
// Using axios to asynchrously query the API
mounted () {
axios
.get("https://jsonplaceholder.typicode.com/todos")
.then(response => this.items = response.data)
},
computed: {
activeItems() {
this.saveItems;
return this.items.filter(item => {
return !item.completed;
});
},
filteredItems() {
// An active state denotes the task is not yet completed
if (this.show === 'active')
return this.items.filter(item => {
return !item.completed;
});
if (this.show === 'completed')
return this.items.filter(item => {
return item.completed;
});
// This makes it so added tasks go to the top, not bottom
return this.items.reverse();
},
},
methods: {
addItem() {
if(this.title !== '') { //check if `title` is empty or not
this.items.push({
title: this.title,
completed: false
})
this.title = "";
}
else {
alert("Please enter a task.")
}
I created an example in Stackblitz. You can also view the example directly by clicking here
Can I make 3 errors types without if/else?
I need to show 3 types of errors (they have different positions):
common
errorLogin
errorPassword
What I have now:
I made a renderless function.
And I see 2 ways:
use if/else in the template of the component:
Component.vue
<login-error-renderless
v-if="isError"
:errorMessage="errorMessage"
:rowClass="$style.errorLogin"
:colClass="$style.errorInner"
>
</login-error-renderless>
<login-error-renderless
v-else-if="isError && messageList[1]"
:errorMessage="errorMessage"
:rowClass="$style.errorPassword"
:colClass="$style.errorInner"
>
</login-error-renderless>
<login-error-renderless
v-else-if="isError && !messageList[0] && !messageList[1]"
:errorMessage="errorMessage"
:rowClass="$style.errorWrap"
:colClass="$style.errorInner"
>
</login-error-renderless>
RenderlessFunction.vue
return h('div', { attrs: { class: this.rowClass } }, [
h('div', { attrs: { class: this.colClass} }, [
h('span', {}, this.errorMessage),
]),
use if/else in the renderless function (I'm using this now):
Component.vue
<login-error-renderless
v-if="isError"
:errorMessage="errorMessage"
:classList="classList"
:messageList = "messages"
>
</login-error-renderless>
Renderless.vue:
props: {
errorMessage: String,
classList: Object,
messageList: Array,
},
render(h) {
const createFunction = (rowClass, colClass = this.classList.innerClass) => h('div', { attrs: { class: rowClass } }, [
h('div', { attrs: { class: colClass } }, [
h('span', {}, this.errorMessage),
]),
]);
if (this.messageList[0] !== undefined) {
return createFunction(this.classList.errorLogin);
}
if (this.messageList[1] !== undefined) {
return createFunction(this.classList.errorPassword);
}
return createFunction(this.classList.error);
},
I'm trying to make a dynamic menu at vue, but, while I trying to apply a "active" css class to the menu item, not work, I see the menu variable "menuFomartado" is not updating by VUE, what I thinking wrong?
This code only works while loading page again..
<script>
export default {
data() {
return {
menuFomartado: [],
menu: [
{
icon: "fa fa-chart-area",
pageName: "dashboard",
title: "Dashboard",
role: "admin"
},
{
icon: "fa fa-user-circle",
pageName: "clientes",
title: "Clientes",
role: "admin"
},
]
};
},
computed: {
sideMenu() {
return this.nestedMenu(this.menu);
}
},
watch: {
$route() {
this.menuFomartado = this.sideMenu;
}
},
mounted() {
this.menuFomartado = this.nestedMenu(this.sideMenu);
},
methods: {
nestedMenu(menu) {
menu.forEach((item, key) => {
if (typeof item !== "string" && item.pageName != "") {
menu[key].active = item.pageName == this.$route.name;
}
});
return menu;
}
}
};
</script>
Based in #pierre-said from post Vuejs and Vue.set(), update array
I just changed the line
menu[key].active = item.pageName == this.$route.name;
To
this.$set(menu[key], "active", item.pageName == this.$route.name);
And works fine
I have an array:
basicForm.schema = [
{},
{} // I want to watch only this
]
I tried doing this:
‘basicForm.schema[1].value’: {
handler (schema) {
const plan = schema.find(field => {
return field.name === ‘plan’
})
},
deep: true
},
But I got this error:
vue.js?3de6:573 [Vue warn]: Failed watching path:
“basicForm.schema[1]” Watcher only accepts simple dot-delimited paths.
For full control, use a function instead.
What's the correct way of doing this?
You can watch a computed property instead:
new Vue({
el: '#app',
data: {
basicForm: {
schema: [
{a: 1},{b: 2} // I want to watch only this
]
}
},
computed: {
bToWatch: function() {
return this.basicForm.schema[1].b
}
},
methods: {
incB: function() {
this.basicForm.schema[1].b++
}
},
watch: {
bToWatch: function(newVal, oldVal) {
console.log(newVal)
}
}
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<button #click="incB()">Inc</button>
</div>
You should use a function as the warning message suggests. You need to do so via vm.$watch.
new Vue({
el: '#app',
data: {
items: [
{ name: 'bob' },
{ name: 'fred' },
{ name: 'sue' },
],
},
created() {
this.$watch(() => this.items[1].name, this.onNameChanged);
},
methods: {
changeValue() {
this.items[1].name = 'rose';
},
onNameChanged(name) {
alert('name changed to ' + name);
},
},
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<button #click="changeValue">Click me</button>
</div>
You should probably check that this.items[1] exists before accessing it inside the watch function otherwise you'll get an error.