How do you create a nested binding scope in vue.js? - vue.js

A Vue instance can allow you to create nested view models. For example, consider the following
new Vue({
el: '#app',
data: {
form: {
firstName: "Joe",
lastName: "Bloggs"
}
},
computed: {
name: function () {
return this.form.firstName + ' ' + this.form.lastName;
}
}
});
As you can see, there is a nested form-data object: form.firstName and form.lastName. I can bind this view-model to HTML with the following:
<div id="app">
<form>
<label>
First:
<input type="text" v-model="form.firstName">
</label>
<label>
Last:
<input type="text" v-model="form.lastName">
</label>
</form>
<div>
You are: {{name}}
</div>
</div>
Here's a JS Fiddle for this Vue.js example
Now, my question is: is there a simple way (e.g. a directive) to create a nested binding scope that allows me to address firstName and lastName without the preceding "form."?
Knockout.js has the with binding that allows you to explicitly specify a binding scope in relationship to your view-model. Here is a JS Fiddle showing Knockout.js using the with binding
Is there a simple analogue to Knockout's with binding in Vue?

You could achieve this behavior by using the composition API function reactive and the utility toRefs with setup option as follows :
<script>
import { reactive, toRefs, computed } from "vue";
export default {
setup() {
const form = reactive({
firstName: "aa",
lastName: "bb",
});
const name = computed(() => form.firstName + " " + form.lastName);
return { ...toRefs(form), name };
},
};
</script>
<template>
<div id="app">
<form>
<label>
First:
<input type="text" v-model="firstName" />
</label>
<label>
Last:
<input type="text" v-model="lastName" />
</label>
</form>
<div>You are: {{ name }}</div>
</div>
</template>
LIVE DEMO
in script setup syntax just destruct the object returned from toRefs and properties will be exposed directly to the template :
<script setup>
import { reactive, toRefs, computed } from "vue";
const form = reactive({
firstName: "aa",
lastName: "bb",
});
const name = computed(() => form.firstName + " " + form.lastName);
const { firstName, lastName } = toRefs(form);
</script>
Options API example
This is an example of splatting/spreading a prop inside a component into data attributes. It uses a "model object" containing the data, which is likely unidiomatic Vue code, but might be more familiar to people coming from KnockoutJS:
const { createApp, ref, markRaw } = Vue;
class MyModelObject {
constructor() {
this.valueA = ref();
this.valueB = ref();
// prevent Vue from making instances of this class
// deeply reactive when they are assigned to the root
// components `data`
markRaw(this);
}
};
const MyComponent = {
props: {
object: Object
},
data() {
return { ...this.object };
},
template: document.querySelector("template")
};
createApp({
components: {
MyComponent: MyComponent
},
data() {
return {
object: null
};
},
created() {
this.object = new MyModelObject();
this.object.valueA.value = "foo";
this.object.valueB.value = "bar";
}
}).mount(document.querySelector("main"));
<script src="https://unpkg.com/vue#3/dist/vue.global.js"></script>
<main>
<my-component :object="object"></my-component>
</main>
<template>
{{ valueA }} {{ valueB }}
</template>

As long as you don't have repeated values, you could alias it to a computed property like
computed: {
firstName: function() {
return form.firstName
},
lastName: function() {
return form.lastName
}
}

Related

How do I render data inside an html string in Vue, before it is displayed?

I have an html string that contains some variables wrapped in {{}}. Is there a way to trigger the parsing of the html to replace {{}} with values that are present in the teamplate already
<div v-html="desc"></div>
desc = "<p>Some text {{aVar}}</p>"
I would like to be able to do something like
v-html="parse(desc)"
and {{aVar}} be replaced with the actual value that is available
Hopefully there is Vue method to do this, but I can definitely use a custom method and replace the values myself.
Thank you
For now I solved it with
function parseHtml(html = "") {
const expose = {
player,
};
return html.replace(/{{(.+?)}}/g, (_, g) => {
return _get(expose, g);
});
}
where _get is the lodash _.get
Like this?
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
const parse = (text) => (`<span style=\"color:red\">${text}<span>`)
</script>
<template>
<input type="text" v-model="msg">
<div v-html="parse(msg)"></div>
</template>
With inspiration from your example #orbitory
What about this?
Options API
<script>
export default {
data() {
return {
template: `<p> {{ message }} {{ message2 }}</p>`,
message: "hello",
message2: "world",
};
},
methods: {
parse(html) {
return html.replace(/{{(.+?)}}/g, (_, g) => {
return this[g.trim()];
});
},
},
};
</script>
<template>
<input v-model="message">
<input v-model="message2">
<div v-html="parse(template)" />
</template>
Demo with reactive input fields example.
https://codesandbox.io/s/how-do-i-render-data-inside-an-html-string-in-vue-before-it-is-displayed-x8oq1?file=/src/App.vue
Composition API
<script setup>
import { ref } from 'vue'
let template = "<p> {{ message }} {{ message2 }} </p>"
let message = ref('hello')
let message2 = ref('world')
let data = { message, message2 }
function parse(html) {
return html.replace(/{{(.+?)}}/g, (_, g) => {
return this[g.trim()].value;
});
}
parse = parse.bind(data)
</script>
<template>
<input v-model="message">
<input v-model="message2">
<div v-html="parse(template)"></div>
</template>
Demo with reactive input fields - based on #tauzN example.
link

How to use composition API with custom field in Vee Validate 4 correctly

After reading documentation of Vee Validate 4 about using composition api with custom inputs, do I understand correctly that hook useField a have to call only inside input component(in my example is VInput.vue)? Is any way that i can use hook functionality in parent component? The VInput is used for another functionality that don't need validation so it will be extra functionality add useForm for global component in out project
For example I have List.vue
<template>
<form class="shadow-lg p-3 mb-5 bg-white rounded" #submit.prevent="submitPostForm">
<VFormGroup label="Title" :error="titleError">
<VInput v-model="postTitle" type="text" name="title" />
</VFormGroup>
<VFormGroup label="Body" :error="bodyError">
<VInput v-model="postBody" type="text" name="body" />
</VFormGroup>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</template>
<script>
import { ref, onMounted } from 'vue';
import { Form, useForm, useField } from 'vee-validate';
import * as Yup from 'yup';
export default {
components: { Form },
setup() {
const schema = Yup.object().shape({
title: Yup.string().required(),
body: Yup.string().min(6).required(),
});
//hooks
const { handleSubmit } = useForm({ validationSchema: schema });
// No need to define rules for fields because of schema
const { value: postTitle, errorMessage: titleError } = useField('title');
const { value: postBody, errorMessage: bodyError } = useField('body');
//methods
const submitPostForm = handleSubmit(() => {
addPost({ title: postTitle, body: postBody });
});
return { schema, postTitle, postBody, titleError, bodyError, submitPostForm };
},
};
</script>
The problem that input error I have to show only in VFormGroup so how I can manage this form?
I am not sure if understood your question correctly.
But in your case you only have 1 issue, you destructure errorMessage and value twice in one file, which isn't working.
you could change your useField's like this:
const { errorMessage: titleError, value: titleValue } = useField('title', Yup.string().required());
const { errorMessage: bodyError, value: bodyValue } = useField('body', Yup.string().required().min(8));
In your template you then want to use titleError and bodyError in your VFormGroup.
In your VInput you want to use titleValue and bodyValue for v-model
because you initialise your title and body in your setup() I guess that those do not have any predefiend values. If that would be the case you might want to take a look at the Options for useField() where you can have as a thridParam as an Object with e.g. initialValue as key which then would be your post.value.title. But for your use case I wouldn't recommend this.
to answer the Code question from the comments:
<template>
<form class="shadow-lg p-3 mb-5 bg-white rounded" #submit="handleSubmit">
<VFormGroup label="Title" :error="titleError">
<VInput v-model="titleValue" type="text" name="title" />
</VFormGroup>
<VFormGroup label="Body" :error="bodyError">
<VInput v-model="bodyValue" type="text" name="body" />
</VFormGroup>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</template>
<script>
import { ref } from 'vue';
import { Form, useForm, useField } from 'vee-validate';
import * as Yup from 'yup';
export default {
components: { Form },
setup() {
const title = ref('');
const body = ref('');
//hooks
const { handleSubmit } = useForm();
// No need to define rules for fields because of schema
const { errorMessage: titleError, value: titleValue } = useField('title', Yup.string().required());
const { errorMessage: bodyError, value: bodyValue } = useField('body', Yup.string().required().min(8));
return { titleError, bodyError, titleValue, bodyValue, handleSubmit };
},
};
</script>

Vue composition API is not reactive inside v-for?

I am using Vue 2 and Vuetify (not Vue 3) to create a form builder website. I was going perfectly well until I found out that something is wrong. So here's the case. I rendered text fields (inputs) from a reactive array using the following code.
<template>
// ... some other unrelated code
<template v-if="answer.type !== 1">
<v-col
v-for="(_, i) in answer.options"
:key="`#question-${answer.id}-${i}`"
cols="12"
>
<div class="flex flex-row justify-between items-center">
<TextField
v-model="answer.options[i]"
class="ml-4"
label="Option"
underlined
hideLabel
/>
<v-btn #click="deleteOption(i)" icon>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</v-col>
<v-col>
<TextButton #click="addOption()" text="+ ADD OPTION" />
</v-col>
</template>
// ... some other unrelated code
</template>
<script>
import { reactive, ref, watch } from '#vue/composition-api'
import useInquiry from '#/composables/useInquiry'
import TextButton from '#/components/clickables/TextButton.vue'
import TextField from '#/components/inputs/TextField.vue'
export default {
components: { TextField, TextButton },
setup() {
const { answer, addOption, deleteOption } = useInquiry()
return { answer, addOption, deleteOption }
}
}
</script>
Here's my useInquiry composable logic
import { reactive, watch, se } from '#vue/composition-api'
import ID from '#/helpers/id'
export default () => {
const answer = reactive({
id: ID(), // this literally just generates an ID
type: 2,
options: ['', '']
})
const addOption = () => {
answer.options.push('')
}
const deleteOption = at => {
const temp = answer.options.filter((_, i) => i !== at)
answer.options = []
answer.options = temp
};
return { answer, addOption, deleteOption }
}
And finally, here's my TextField.vue
<template>
<v-text-field
v-model="inputValue"
:label="label"
:single-line="hideLabel"
:type="password ? 'password' : 'text'"
:outlined="!underlined"
:dense="!large"
hide-details="auto"
/>
</template>
<script>
import { ref, watch } from '#vue/composition-api'
export default {
model: {
props: 'value',
event: 'change'
},
props: {
label: String,
value: String,
password: Boolean,
underlined: Boolean,
large: Boolean,
hideLabel: Boolean
},
setup(props, context) {
const inputValue = ref(props.value)
watch(inputValue, (currInput, prevInput) => {
context.emit('change', currInput)
})
return { inputValue }
}
}
</script>
The problem is, everytime the delete button is clicked, the deleted input is always the last one on the array, even though I didn't click on last one. I tried to log my reactive array by watching it using Vue's composition watch method. Apparently, the data is correctly updated. The problem is the v-model looks un-synced and the last input is always the one that gets deleted.
Looks good to me ....except the TextField.vue
Problem is in TextField.vue. What you actually doing inside setup of TextField.vue is this (Vue 2 API):
data() {
return {
inputValue: this.value
}
}
...this is one time initialization of inputValue data member with value of value prop. So when one of the options is removed and components are reused (because that's what Vue does all the time - especially when index is used in :key) inputValue is not updated to a new value of value prop...
You don't need inputValue or model option at all, just remove it and use this template:
<v-text-field
:value="value"
#input="$emit('input', $event)"
:label="label"
:single-line="hideLabel"
:type="password ? 'password' : 'text'"
:outlined="!underlined"
:dense="!large"
hide-details="auto"
/>
NOTE that in my example I'm using $event.target.value instead of just $event because I'm working with native <input> element and not Vuetify's custom component input...
working example...
const {
reactive,
ref,
watch
} = VueCompositionAPI
Vue.use(VueCompositionAPI)
const BrokenInput = {
model: {
props: 'value',
event: 'change'
},
props: {
value: String,
},
setup(props, context) {
const inputValue = ref(props.value)
watch(inputValue, (currInput, prevInput) => {
context.emit('change', currInput)
})
return {
inputValue
}
},
template: `<input type="text" v-model="inputValue" />`
}
const FixedInput = {
props: {
value: String,
},
template: `<input type="text" :value="value" #input="$emit('input', $event.target.value)"/>`
}
const useInquiry = () => {
const answer = reactive({
id: 1,
type: 2,
options: ['1', '2', '3', '4', '5']
})
const addOption = () => {
answer.options.push('')
}
const deleteOption = at => {
const temp = answer.options.filter((_, i) => i !== at)
answer.options = temp
};
return {
answer,
addOption,
deleteOption
}
}
const app = new Vue({
components: { 'broken-input': BrokenInput, 'fixed-input': FixedInput },
setup() {
return useInquiry()
},
})
app.$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/#vue/composition-api#1.0.0-beta.18"></script>
<div id="app">
<table>
<tr>
<th>Option</th>
<th>Broken input</th>
<th>Model</th>
<th>Fixed input</th>
<th></th>
<tr>
<tr v-for="(_, i) in answer.options" :key="'#question-'+ answer.id + '-' + i">
<td>Option {{ i }}:</td>
<td>
<broken-input v-model="answer.options[i]" />
</td>
<td>
{{ answer.options[i] }}
</td>
<td>
<fixed-input v-model="answer.options[i]" />
</td>
<td><button #click="deleteOption(i)">Remove option</button></td>
</tr>
</table>
<button #click="addOption()">Add option</button>
</div>

Send value from API request from a component to another Vue.JS

I have a component which allow to retrieve the datas from a rest API...
My template allow user to enter an input (id) and to find the user associated with the user. I also have a component which is called dynamically.
<template>
<div>
<!-- form -->
<form>
<input type="text" v-model="userId" id="userId">
<button type="submit" class="btn btn-primary" #click="getUser($event); !isExistingUser">Get User</button>
</form>
<!-- result -->
<div v-if="!showComponent">
{{ user["id"] }} {{ user["username"] }} {{ user["email"] }}
<button #click="showComponent = !showComponent">Editer</button>
</div>
<!-- Edit the user -->
<div v-if="showComponent">
<edit-user :toUpdate="updateUser"></edit-user>
</div>
</div>
</template>
In the script part I have datas and methods :
The objective is to send the user that i collect and to send it to the update user. For this I created a data binding.
I also try to set the value of the object in the getUser method. And i can display the value.
<script>
import axios from "axios";
import EditUserForUpdate from "./EditUserForUpdate";
export default {
name: "FindUser",
components: {
"edit-user": EditUserForUpdate
},
data() {
return {
toUpdate: Object,
user: null,
isExistingUser: false,
userId: "",
userEmail:"",
userUsername: "",
showComponent: false
};
},
methods: {
getUser(event) {
axios
.get("http://localhost:4000/api/users/" + this.userId)
.then(response => {
console.log(response);
this.user = response.data.data;
var toUpdate = {};
toUpdate = { upUserName: this.user.username, upUserEmail: this.user.email, upId: this.user.id};
console.log(toUpdate);
});
}
}
};
</script>
Finally in the child component :
<script>
export default {
name: "EditUserForUpdate",
data: function () {
return {
updateUser: ''
}
},
props: {
updateUser: Object
},
methods: {
beforeMount () {
var updateUser = this.updateUser // save props data to itself's data and deal with it
console.log("userToUpdate : " + updateUser);
}
}
}
</script>
My issue is that I don't retrieve the data in the child module for an unknown reason.
The property is named toUpdate and not updateUser.
Update your prop accordingly in the EditUserForUpdate component:
props: {
toUpdate: Object
}
And of course, localize that object for manipulation:
beforeMount() {
this.updateUser = this.toUpdate
}

How do i get the ViewModel from Vue's :is

i have these components:
<template id="test-button-component">
<div class="test-button__container">
This is test button
<button #click="clickButton">{{buttonTitle}}</button>
</div>
</template>
<template id="test-button-component2">
<div class="test-button__container">
<button></button>
</div>
</template>
I try to use the Vue's :is binding to do a component binding by name as follow:
<div :is='myComponentName' ></div>
every time the myComponentName changed to other component, the new component will replace the old component. The thing i need is, is there any way i can get the instance of the component so i can get the view model instance of the currently bound component?
You can add a ref attribute (for example ref="custom") to the <div> tag for the dynamic component. And then reference the component instance via this.$refs.custom.
Here's a simple example where the data of the component gets logged whenever the value being bound to the is prop is changed:
new Vue({
el: '#app',
data() {
return {
value: 'foo',
children: {
foo: {
name: 'foo',
template: '<div>foo</div>',
data() {
return { value: 1 };
}
},
bar: {
name: 'bar',
template: '<div>bar</div>',
data() {
return { value: 2 };
}
}
}
}
},
computed: {
custom() {
return this.children[this.value];
}
},
watch: {
custom() {
this.$nextTick(() => {
console.log(this.$refs.custom.$data)
});
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<select v-model="value">
<option>foo</option>
<option>bar</option>
</select>
<div :is="custom" ref="custom"></div>
</div>
Note that the $data for the component reference by $refs.custom is getting logged inside of a $nextTick handler. This is because the bound component won't update until the parent view has re-rendered.