Vuejs - How to set the default value of a prop to a predefined data? - vue.js

The question is simple. I want to define a data as follows;
data() {
return {
logoURL: "some-link/some-picture.png"
}
}
and I want to set it to a prop's default state like this:
props: {
infoLogoURL: {
type: String,
default: this.logoURL,
},
}
Apparently it doesn't work the way I want and I have this error:
Uncaught TypeError: Cannot read property 'logoURL' of undefined
How can I manage this? Here is an example of how I use my props:
<cardComp
infoTitle = "Info Title"
infoText = "Info Text"
infoSubIcon = "Sub Icon Name"
infoSubIconColor = "css-color-class"
infoSubText = "Sub Text"
infoDescription = "Some Text Description"
infoIcon = "Icon Name"
infoIconColor = "icon-color-css"
infoLogoURL = "some-link/some-picture.png"
/>
And here is another question... I want to display infoIcon when there is no infoLogoURL. So let's say the link of that specific .png file is not available in a moment of time, so in that case, I want to display the infoIcon. When the .png file is available, I should only display the infoLogoURL, not the infoIcon. How do I do that?

You can't set the default value of a prop from data.
One way around this is to use a computed property instead:
computed: {
defaultLogoURL: function() {
return this.infoLogoURL || this.logoURL
}
}

You can define a computed property that returns the value of the prop "info_logo_url" when set, and that of data "logoURL" when not.
Concerning the second part of the question, another computed property can be defined to return the prop "info_logo_url" when set, and that of the prop "info_icon" when not.
const cardcomponent = Vue.component('card-component', {
template: '#card-component',
data(){
return { logoURL: "some-link/some-picture.png" }
},
props: {
info_logo_url: { type: String },
info_icon: { type: String }
},
computed: {
myInfoLogo() { return this.info_logo_url || this.logoURL; },
myInfoIcon() { return this.info_logo_url || this.info_icon; },
}
});
new Vue({
el: '#app',
components: { cardcomponent },
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
<cardcomponent info_logo_url="info logo URL" info_icon="info icon"/>
</div><hr>
<div>
<cardcomponent info_logo_url="info logo URL"/>
</div><hr>
<div>
<cardcomponent info_icon="info icon"/>
</div>
</div>
<template id="card-component">
<div>
<b>myInfoLogo</b>: {{myInfoLogo}} - <b>myInfoIcon</b>: {{myInfoIcon}}
</div>
</template>

Related

VueJS toggle password visibilty without mutating the "type" property

I have a basic input component I am working with that has type as a property and up until now has been working very well. However, trying to use it for passwords and implementing obfuscation has been sort of tricky.
How can I toggle hide/show of the password without mutating the prop? I figured making it type = 'password' to type = 'text was the best way, but clearly not.
I've made a Codesandbox to replicate that part of the component, but any advice or direction would be greatly appreciated!
PasswordInput.vue:
<template>
<div>
<input :type="type" />
<button #click="obfuscateToggle" class="ml-auto pl-sm _eye">
<div>
<img :src="`/${eyeSvg}.svg`" alt="" />
</div>
</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
passwordVisible: false,
eyeSvg: "eye-closed",
};
},
props: {
type: { type: String, default: "text" },
},
methods: {
obfuscateToggle() {
if (this.eyeSvg === "eye-closed") {
this.eyeSvg = "eye";
} else this.eyeSvg = "eye-closed";
// this.eyeSvg = "eye-closed" ? "" : (this.eyeSvg = "eye");
if ((this.type = "password")) {
this.type = "text";
} else this.type = "password";
},
},
};
</script>
App.vue
<template>
<div id="app">
<PasswordInput type="password" />
</div>
</template>
The only way to do it is by mutating the type attribute. As that is how the browser decides the render it as either just a textbox or as a password. Therefore you are doing this the right way.
The one issue that you will encounter is that you will have errors thrown in your console because you are attempting to mutate a prop.
This is quick and easy to fix. First, you will create a new data property, and assign it to the default of type
data(){
return{
fieldType:'text'
}
}
Then you will use the on mounted lifecycle hook, and update your data property to match your prop's value`
mounted(){
this.fieldType = this.type;
}
If you know the type prop will change from the parent component you can also use a watcher for changes and assign type
watch:{
type(val){
this.fieldType = val;
}
}
You will then update your obfuscateToggle method to use the fieldtype variable:
obfuscateToggle() {
if (this.eyeSvg === "eye-closed") {
this.eyeSvg = "eye";
} else this.eyeSvg = "eye-closed";
//You can simplify this by using this.fieldType = this.fieldType == "text" ? "password" : "text"
if (this.fieldType == "password") {
this.fieldType = "text";
} else this.fieldType = "password";
}
Finally, in your template, you will want to change type to fieldType
<template>
<div>
<input :type="fieldType" />
<button #click="obfuscateToggle" class="ml-auto pl-sm _eye">
<div>
<img :src="`/${eyeSvg}.svg`" alt="" />
</div>
</button>
</div>
</template>
Putting it all together
<script>
export default {
name: "HelloWorld",
data() {
return {
passwordVisible: false,
eyeSvg: "eye-closed",
fieldType: "text"
};
},
props: {
type: { type: String, default: "text" },
},
methods: {
obfuscateToggle() {
if (this.eyeSvg === "eye-closed") {
this.eyeSvg = "eye";
} else this.eyeSvg = "eye-closed";
//You can simplify this by using this.fieldType = this.fieldType == "text" ? "password" : "text"
if (this.fieldType == "password") {
this.fieldType = "text";
} else this.fieldType = "password";
},
},
watch:{
type(val){
this.fieldType = val;
}
},
mounted(){
this.fieldType = this.type;
},
};
</script>
Here is an example on CodeSandBox
Also, you had a small typo in your obfuscateToggle method.
if(this.type = 'password')
this was assigning type instead of comparing it against a literal :)

How to use v-model and computed properties on Input fields?

I have a simple component with 2 input fields:
<template>
<div>
<input type="text" v-model="name">
<input type="text" v-model="alias">
</div>
</template>
<script>
export default {
data() {
return {
name: "",
alias: ""
}
}
}
</script>
I want to automatically insert the name model's value to the alias field IF the alias field is empty. If it's not empty, I want it to use its own value.
Here is my attempt:
<template>
<div>
<input type="text" v-model="name">
<input type="text" v-model="alias">
</div>
</template>
<script>
export default {
data() {
return {
name: "",
alias: ""
}
},
computed: {
alias: {
get: function() {
if (this.alias.length < 1) {
return this.name
} else {
return this.alias
}
},
set: function(newValue) {
this.alias = newValue
}
}
}
}
</script>
The problem is that alias doesn't get the actual value in the alias property if I type something into the name field. I can only see it visually - but it doesn't hold the value. However, if I type into the alias field, it gets updated properly.
What am I missing here and how can I make it the way I want it?
Computed won't work because it should be treated as immutable.
Also because the model will be updated on each input, a watch won't work either, it would only pick up the first char of what you enter, unless its pre-populated.
This is how I would do it, simply add a #blur event on the name input then fire a method which populates alias if it's empty and on alias in case they empty it out.
The same method could be used in mounted, if you pre-populate the models, or you could watch it.
{
template: `
<div>
<input type="text" v-model="name" #blur="setAlias()">
<input type="text" v-model="alias" #blur="setAlias()">
</div>
`,
data() {
return {
name: '',
alias: ''
}
},
methods: {
setAlias() {
if (this.alias === '') {
this.alias = this.name
}
}
}
}
Firstly, you cannot have a computed property and a data property with the same name. Since both computed and data properties end up as properties on the same state object, one will overwrite the other.
Secondly, and I think you did this because of the first point, in your computed alias getter, your reference the alias again, which is essentially referencing itself, and looks like it could give some inconsistent return values.
I can think of two solutions to your issue:
1) Use a watcher on name:
Create a watcher function for name, and in it set this.alias to the same value as name when alias is blank, or if it's the same as the previous name value.
<script>
export default {
data: () => ({
name: "",
alias: ""
}),
watch: {
name(newVal, oldVal) {
if (!this.alias || this.alias === oldVal) {
this.alias = newVal;
}
}
}
}
</script>
2) Use explicit :value and #change/#keyup bindings on the name input:
v-model is a convenience method that sets both of these for you, but in your case you want to do some more logic in the change handler that just setting a state property value.
<template>
<div>
<input
type="text"
:value="name"
#keyup="onNameInput"
/>
<input type="text" v-model="alias">
</div>
</template>
<script>
export default {
data: () => ({
name: "",
alias: ""
}),
methods: {
// Check and set both values on name input events
onNameInput(e) {
if (!this.alias || this.alias === this.name) {
this.alias = e.target.value;
}
this.name = e.target.value;
}
}
}
</script>

VueJS: Use v-model and :value in the same time

I'am searching a way to use v-model and :value in same time on the same object.
I got this error:
:value="user.firstName" conflicts with v-model on the same element
because the latter already expands to a value binding internally.
The purpose is to set as default value the value get from the mapGetters (coming from one store) and to set the right value when the user will submit the modification. (in onSubmit)
<div class="form-group m-form__group row">
<label for="example-text-input" class="col-2 col-form-label">
{{ $t("firstname") }}
</label>
<div class="col-7">
<input class="form-control m-input" type="text" v-model="firstname" :value="user.firstName">
</div>
</div>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
data () {
return {
lang: "",
firstname: ""
}
},
computed: mapGetters([
'user'
]),
methods: {
...mapActions([
'updateUserProfile'
]),
onChangeLanguage () {
this.$i18n.locale = lang;
},
// Function called when user click on the "Save changes" btn
onSubmit () {
console.log('Component(Profile)::onSaveChanges() - called');
const userData = {
firstName: this.firstname
}
console.log('Component(Profile)::onSaveChanges() - called', userData);
//this.updateUserProfile(userData);
},
// Function called when user click on the "Cancel" btn
onCancel () {
console.log('Component(Profile)::onCancel() - called');
this.$router.go(-1);
}
}
}
</script>
Typically you want to set the "initial" value of the v-model on the object itself, like:
data() {
return {
firstname: 'someName'
}
}
But since you're getting it from the store, you could access the specific getter object with this.$store.getters[your_object], so I would remove the :value binding and use v-model alone for this:
<div class="col-7">
<input class="form-control m-input" type="text" v-model="firstname">
</div>
<script>
export default {
data() {
return {
lang: "",
firstname: this.$store.getters.user.firstName
}
},
// ...
}
</script>
The Vue v-model directive is syntactic sugar over v-bind:value and v-on:input. This alligator.io article helped me a lot to understand how it works.
So basically your problem is that the v-model directive sets value to firstname, while you're also explicitly setting value to user.firstName.
There are a lot of ways to handle this issue. I think a fast and straightforward solution is to store the firstname as a data variable (as you're already doing), and then use only v-model with it, disregarding v-bind:value.
Then, to set the user from the store for the default username, you could set fristname as the store user's username in the created hook:
script:
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
created() {
this.firstname = this.user.username; // is this right? no used to the map getters syntax, but this is the idea
},
data () {
return {
lang: "",
firstname: ""
}
},
computed: mapGetters([
'user'
]),
methods: {
...mapActions([
'updateUserProfile'
]),
onChangeLanguage () {
this.$i18n.locale = lang;
},
// Function called when user click on the "Save changes" btn
onSubmit () {
console.log('Component(Profile)::onSaveChanges() - called');
const userData = {
firstName: this.firstname
}
console.log('Component(Profile)::onSaveChanges() - called', userData);
//this.updateUserProfile(userData);
},
// Function called when user click on the "Cancel" btn
onCancel () {
console.log('Component(Profile)::onCancel() - called');
this.$router.go(-1);
}
}
}
</script>
You should only use v-model, it will create a 2-way binding with the value in your script: changing the variable in js will update the input element, interacting with the input element will update the variable.
If you want to use a default value, just set the variable to that value (wherever it may come from).

Is it possible to use a v-model from inside a component template?

Can the v-model syntax be used from inside a Vue component template?
The following works as expected when included directly in an .html
<input type="text" v-model="selected_service_shortname">
Putting the following stuff into a component template does not work.
var service_details = {
template: `
...
<input type="text" v-model="selected_service_shortname">
...
`
};
vm = new Vue({
el: "#app",
components: {
'service-details': service_details
},
Results in vue.min.js:6 ReferenceError: selected_service_shortname is not defined
Changing the template syntax to
<input type="text" v-model="this.$parent.selected_service_shortname">
Seems to halfway work -- changes applied externally to selected_service_shortname appear in the input box as expected. But making changes to the input box directly results in Uncaught TypeError: Cannot convert undefined or null to object
Is what I'm trying to do a supported use case? If so, are there working examples somewhere?
You can implement support for v-model in your component. This is covered in the documentation here.
Here is an example.
var service_details = {
props: ["value"],
template: `
<input type="text" v-model="internalValue">
`,
computed: {
internalValue: {
get() {
return this.value
},
set(v) {
this.$emit("input", v)
}
}
}
};
Basically, v-model, by default, is simply sugar for passing a value property and listening for the input event. So all you need to do is add a value property to your component, and emit an input event. This can also be customized as described in the documentation.
console.clear()
var service_details = {
props: ["value"],
template: `
<input type="text" v-model="internalValue">
`,
computed: {
internalValue: {
get() {
return this.value
},
set(v) {
this.$emit("input", v)
}
}
}
};
new Vue({
el: "#app",
data: {
selected_service_shortname: "some service name"
},
components: {
'service-details': service_details
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.min.js"></script>
<div id="app">
<service-details v-model="selected_service_shortname"></service-details>
<hr>
Selected Service Shortname: {{selected_service_shortname}}
</div>
Used in the parent like this:
<service-details v-model="selected_service_shortname"></service-details>

Passing data into a Vue template

I am fairly new to vue and can't figure out how to add data values within a template. I am trying to build a very basic form builder. If I click on a button it should add another array of data into a components variable. This is working. The I am doing a v-for to add input fields where some of the attributes are apart of the array for that component. I get it so it will add the input but no values are being passed into the input.
I have created a jsfiddle with where I am stuck at. https://jsfiddle.net/a9koj9gv/2/
<div id="app">
<button #click="add_text_input">New Text Input Field</button>
<my-component v-for="comp in components"></my-component>
<pre>{{ $data | json }}</pre>
</div>
new Vue({
el: "#app",
data: function() {
return {
components: [{
name: "first_name",
showname: "First Name",
type: "text",
required: "false",
fee: "0"
}]
}
},
components: {
'my-component': {
template: '<div>{{ showname }}: <input v-bind:name="name" v-bind:type="type"></div>',
props: ['showname', 'type', 'name']
}
},
methods: {
add_text_input: function() {
var array = {
name: "last_name",
showname: "Last Name",
type: "text",
required: "false",
fee: "0"
};
this.components.push(array);
}
}
})
I appreciate any help as I know I am just missing something obvious.
Thanks
Use props to pass data into the component.
Currently you have <my-component v-for="comp in components"></my-component>, which doesn't bind any props to the component.
Instead, do:
<my-component :showname="comp.showname"
:type="comp.type"
:name="comp.name"
v-for="comp in components"
></my-component>
Here is a fork of your fiddle with the change.
while asemahle got it right, here is a boiled down version on how to expose data to the child component. SFC:
async created() {
await this.setTemplate();
},
methods: {
async setTemplate() {
// const templateString = await axios.get..
this.t = {
template: templateString,
props: ['foo'],
}
},
},
data() {
return {
foo: 'bar',
t: null
}
}
};
</script>
<template>
<component :is="t" :foo="foo"></component>
It pulls a template string that is compiled/transpiled into a js-render-function. In this case with Vite with esm to have a client-side compiler available:
resolve: {
alias: {
// https://github.com/vuejs/core/tree/main/packages/vue#with-a-bundler
vue: "vue/dist/vue.esm-bundler.js",
the index.js bundle size increases by few kb, in my case 30kb (which is minimal)
You could now add some what-if-fail and show-while-loading with defineasynccomponent