Support optional chaining in vuejs - vue.js

I have created vue and electron app using #vue/cli-service 4.2 in that I am facing a issue of optional chaining.
I can't use ? for validating the condition like (#babel/plugin-proposal-optional-chaining)
eg. a?.b?.c its means it check weather a exist then check for b otherwise
return false same as template expression in angular.
Any one have idea how to configure optional chaining in vuejs.

One quick update is that Vue 3 comes bundled with support for optional chaining.
To test you can try compiling the below Vue component code.
<template>
<div id="app" v-if="user?.username">
#{{ user?.username }} - {{ fullName }} <strong>Followers: </strong>
{{ followers }}
<button style="align-self: center" #click="followUser">Follow</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
props: {
test: Object
},
data() {
return {
followers: 0,
user: {
id: 1,
test: {},
username: '_sethAkash',
firstName: undefined,
lastName: 'Seth',
email: 'sethakash007#gmail.com',
isAdmin: true
}
}
},
computed: {
fullName(): string {
//
return `${this?.test?.firstName} ${this?.user?.lastName}`
}
},
methods: {
followUser: function () {
this.followers += 1
}
},
watch: {
followers(newFollowerCount, oldFollowerCount) {
if (oldFollowerCount < newFollowerCount) {
console.log(`${this?.user?.username} has gained a follower!`)
}
}
},
mounted() {
this.followUser()
}
})
</script>

Try vue-template-babel-compiler
It uses Babel to enable Optional Chaining(?.), Nullish Coalescing(??) and many new ES syntax for Vue.js SFC.
Github Repo: vue-template-babel-compiler
DEMO
Usage
1. Install
npm install vue-template-babel-compiler --save-dev
2. Config
1. Vue-CLI
DEMO project for Vue-CLI
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compiler = require('vue-template-babel-compiler')
return options
})
}
}
2. Nuxt.js
DEMO project for Nuxt.js
// nuxt.config.js
export default {
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
loaders: {
vue: {
compiler: require('vue-template-babel-compiler')
}
},
},
// ...
}
Please refer to REAMDE for detail usage
Support for Vue-CLI, Nuxt.js, Webpack , any environment use vue-loader v15+.

According to this comment on an issue here
You could create a global mixin and use the eval function to evaluate the expression.
Example:
Vue.mixin({
methods: {
$evaluate: param => eval('this.'+param)
}
});
In the template:
<template>
<p>{{ $evaluate('user?.name') }}</p>
</template>
They also added that it might not be perfect:
Although it's still no substitute for the real operator, especially if you have many occurrences of it
Edit
As stated above, using eval may bring some unintended problems, I suggest you use a computed property instead.
In the SFC:
<template>
<p>{{ userName }}</p>
</template>
<script>
export default {
data(){
return {
user: {
firstName: 'Bran'
}
}
},
computed: {
userName(){
return this.user?.firstName
}
}
}
</script>

/*
* Where to use: Use in vue templates to determine deeply nested undefined/null values
* How to use: Instead of writing parent?.child?.child2 you can write
* isAvailable(parent, 'child.child2')
* #author Smit Patel
* #params {Object} parent
* {String} child
* #return {Boolean} True if all the nested properties exist
*/
export default function isAvailable(parent, child) {
try {
const childArray = String(child).split('.');
let evaluted = parent;
childArray.forEach((x) => {
evaluted = evaluted[x];
});
return !!evaluted;
} catch {
return false;
}
}
Use :
<template>
<div>
<span :v-if="isAvailable(data, 'user.group.name')">
{{ data.user.group.name }}
<span/>
</div>
</template>
<script>
import isAvailable from 'file/path';
export default {
methods: { isAvailable }
}
</script>

This doesn't work exactly the same but I think, in this context, it may be event better for most cases.
I used Proxy for the magic method effect. You just need to call the nullsafe method of an object and from there on, just use normal chaining.
In some versions of VueJs you can't specify a default value. It perceives our null safe value as an object (for good reason) and JSON.stringify it, bypassing the toString method. I could override toJSON method but you can't return the string output. It still encodes your return value to JSON. So you end up with your string in quotes.
const isProxy = Symbol("isProxy");
Object.defineProperty(Object.prototype, 'nullsafe', {
enumarable: false,
writable: false,
value: function(defaultValue, maxDepth = 100) {
let treat = function(unsafe, depth = 0) {
if (depth > maxDepth || (unsafe && unsafe.isProxy)) {
return unsafe;
}
let isNullish = unsafe === null || unsafe === undefined;
let isObject = typeof unsafe === "object";
let handler = {
get: function(target, prop) {
if (prop === "valueOf") {
return target[prop];
} else if (typeof prop === "symbol") {
return prop === isProxy ? true : target[prop];
} else {
return treat(target[prop], depth + 1);
}
}
};
let stringify = function() {
return defaultValue || '';
};
let dummy = {
toString: stringify,
includes: function() {
return false;
},
indexOf: function() {
return -1;
},
valueOf: function() {
return unsafe;
}
};
return (isNullish || isObject) ? (new Proxy(unsafe || dummy, handler)) : unsafe;
};
return treat(this);
}
});
new Vue({
el: '#app',
data: {
yoMama: {
a: 1
}.nullsafe('xx'),
yoyoMa: {
b: 1
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{ yoMama.yoyoMa.yoMama.yoyoMa.yoMama }}
<hr> {{ yoyoMa.nullsafe('yy').yoMama.yoyoMa.yoMama.yoyoMa }}
</div>

After search many possibilities, I maked one function to help me.
Make one js file to save the helper function and export it
const propCheck = function (obj = {}, properties = ""){
const levels = properties.split(".");
let objProperty = Object.assign({}, obj);
for ( let level of levels){
objProperty = objProperty[level];
if(!objProperty)
return false;
}
return true;
}
export default propCheck;
And install this function for globally in the Vue instance
Vue.prototype.$propCheck = propCheck;
After use in your template
<span>{{$propCheck(person, "name")}}</span>
or
<span>{{$propCheck(person, "contatcs.0.address")}}</span>
or
<span>{{$propCheck(person, "addres.street")}}</span>

Use getSafe() method way for template and js files :)
<template><div>
{{getSafe(() => obj.foo.bar)}} <!-- returns 'baz' -->
{{getSafe(() => obj.foo.doesNotExist)}} <!-- returns undefined -->
</div></template>
<script>
export default {
data() {
return {obj: {foo: {bar: 'baz'}}};
},
methods: {getSafe},
};
function getSafe(fn) {
try { return fn(); }
catch (e) {}
}
</script>

July 2022 Update:
It works with Vue 2.7 (https://blog.vuejs.org/posts/vue-2-7-naruto.html)
2.7 also supports using ESNext syntax in template expressions.

You can use loadash's get method in this case:
_.get(object, path, [defaultValue])
Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned in its place.
https://lodash.com/docs/4.17.15#get
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.get(object, 'a[0].b.c');
// => 3
_.get(object, 'a.b.c', 'default');
// => 'default'

Related

Vue 3 Composition API - Props default and DOM inside lifecycle methods

I have a Vue component inside a NuxtJS app and I'm using the #nuxtjs/composition-api.
I have this component which is a <Link> component and I would like to make the code clearer.
I have a computed property that determines to color of my UiIcon from iconColor, iconColorHover, IconActive. But most importantly, I want to set it to a specific color if I have a disable class on my root component. It works like that but it doesn't look too good I believe.
I found out that undefined is the only value that I can use to take UiIcon default props if not defined. Empty string like '' would make more sense to more but it's considered as a valid value. I would have to do some ternary conditions in my UiIcon and I'd like to avoid that.
<template>
<div ref="rootRef" class="row">
<UiIcon
v-if="linkIcon"
:type="linkIcon"
:color="linkIconColor"
class="icon"
/>
<a
class="link"
:href="linkHref"
:target="linkTarget"
:rel="linkTarget === 'blank' ? 'noopener noreferrer' : null"
#mouseover="linkActive = true"
#mouseout="linkActive = false"
>
<slot></slot>
</a>
</div>
</template>
<script lang="ts">
import {
defineComponent,
computed,
ref,
toRefs,
nextTick,
onBeforeMount,
} from '#nuxtjs/composition-api';
import { Colors } from '~/helpers/styles';
export default defineComponent({
name: 'Link',
props: {
href: {
type: String,
default: undefined,
},
target: {
type: String as () => '_blank' | '_self' | '_parent' | '_top',
default: '_self',
},
icon: {
type: String,
default: undefined,
},
iconColor: {
type: String,
default: undefined,
},
iconHoverColor: {
type: String,
default: undefined,
},
},
setup(props) {
const { href, target, icon, iconColor, iconHoverColor } = toRefs(props);
const linkActive = ref(false);
const rootRef = ref<HTMLDivElement | null>(null);
const writableIconColor = ref('');
const linkIconColor = computed({
get: () => {
const linkDisabled = rootRef.value?.classList.contains('disabled');
if (linkDisabled) {
return Colors.DARK_GREY;
}
if (linkActive.value && iconHoverColor.value) {
return iconHoverColor.value;
}
return iconColor.value;
},
set: (value) => {
writableIconColor.value = value;
},
});
onBeforeMount(() => {
nextTick(() => {
const linkDisabled = rootRef.value?.classList.contains('disabled');
if (linkDisabled) {
linkIconColor.value = Colors.DARK_GREY;
}
});
});
return {
rootRef,
linkHref: href,
linkTarget: target,
linkIcon: icon,
linkIconColor,
linkActive,
};
},
});
</script>
Implementing disabled status for a component means it will handle two factors: style (disabled color) and function. Displaying a disabled color is only a matter of style/css. implementing it in programmatical way means it'll take longer time to render completely on user's side and it'll lose more SEO scores. examine UiIcon's DOM from browser and override styles using Deep selectors.
If I were handling this case, I would have described the color with css and try to minimize programmatic manipulation of style.
<template>
<div :disabled="disabled">
</div>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
}
}
}
</script>
// it does not have to be scss.
// just use anything that's
// easier to handle variables.
<style lang="scss">
// I would normally import css with prepend option from webpack,
// but this is just to illustrate the usage.
#import 'custom-styles.scss';
&::v-deep button[disabled] {
color: $disabled-color;
}
</style>
attach validator function on the props object. it'll automatically throw errors on exceptions.
{
props: {
icon: {
type: String,
default: "default-icon",
validator(val) {
return val !== "";
// or something like,
// return val.includes(['iconA', 'iconB'])
},
},
}
}

How to fix Vue 3 template compilation error : v-model value must be a valid JavaScript member expression?

I am working on a vue project and the vue version is 3.0
And recently I can see these many warnings for some reason.
Template compilation error: v-model value must be a valid JavaScript member expression
I guess it is because I am using long v-model variable name like this.
<textarea v-model="firstVariable.subVariable.subVariableKey" readonly></textarea>
Please let me know if any idea.
Thanks in advance
This is the component and template code.
var myTemplate = Vue.defineComponent({
template: '#myTemplate',
data() {
return {
firstVariable: {}
}
},
mounted() {
loadData();
},
methods:{
loadData() {
axios.get(MY_ROUTES).then(res => {
// let's suppose res.data is going to be {subVariable: {subVariableKey: "val"}}
this.firstVariable = res.data;
})
}
}
});
// template.html
<script type="text/template" id="myTemplate">
<div class="container">
<textarea v-model="firstVariable.subVariable?.subVariableKey"></textarea>
</div>
</script>
In order that your property go reactive you've to define its full schema :
data() {
return {
firstVariable: {
subVariable: {
subVariableKey: ''
}
}
}
},
and use it directly without optional chaining
v-model="firstVariable.subVariable.subVariableKey"
because v-model="firstVariable.subVariable?.subVariableKey" malformed expression like v-model="a+b" like this test
Example
var comp1 = Vue.defineComponent({
name: 'comp1',
template: '#myTemplate',
data() {
return {
firstVariable: {
subVariable: {
subVariableKey: ''
}
}
}
},
mounted() {
this.loadData();
},
methods: {
loadData() {
}
}
});
const {
createApp
} = Vue;
const App = {
components: {
comp1
},
data() {
return {
}
},
mounted() {
}
}
const app = createApp(App)
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" >
vue 3 app
<comp1 />
</div>
<script type="text/template" id="myTemplate">
<div class="container">
<textarea v-model="firstVariable.subVariable.subVariableKey"></textarea>
<div>
{{firstVariable.subVariable.subVariableKey}}
</div>
</div>
</script>
You are adding a new property to an object which is not reactive.
Vue cannot detect property addition or deletion. Since Vue performs
the getter/setter conversion process during instance initialization, a
property must be present in the data object in order for Vue to
convert it and make it reactive. For example:
Source
Instead of
this.firstVariable = res.data;
Use
this.$set(this.firstVariable, 'subVariable', res.data.subVariable);

Vue component computed not reacting

I have 2 components OperatorsList and OperatorButton.
The OperatorsList contains of course my buttons and I simply want, when I click one button, to update some data :
I emit select with the operator.id
This event is captured by OperatorList component, who calls setSelectedOperator in the store
First problem here, in Vue tools, I can see the store updated in real time on Vuex tab, but on the Components tab, the operator computed object is not updated until I click antoher node in the tree : I don't know if it's a display issue in Vue tools or a real data update issue.
However, when it's done, I have another computed property on Vue root element called selectedOperator that should return... the selected operator : its value stays always null, I can't figure out why.
Finally, on the button, I have a v-bind:class that should update when the operator.selected property is true : it never does, even though I can see the property set to true.
I just start using Vue, I'm pretty sure I do something wrong, but what ?
I got the same problems before I used Vuex, using props.
Here is my OperatorList code :
<template>
<div>
<div class="conthdr">Operator</div>
<div>
<operator-button v-for="operator in operators" :op="operator.id"
:key="operator.id" #select="selectOp"></operator-button>
</div>
</div>
</template>
<script>
import OperatorButton from './OperatorButton';
export default {
name: 'operators-list',
components : {
'operator-button': OperatorButton
},
computed : {
operators() { return this.$store.getters.operators },
selected() {
this.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
},
},
methods : {
selectOp(arg) {
this.$store.commit('setSelectedOperator', arg);
}
},
}
</script>
OperatorButton code is
<template>
<span>
<button type="button" v-bind:class="{ sel: operator.selected }"
#click="$emit('select', {'id':operator.id})">
{{ operateur.name }}
</button>
</span>
</template>
<script>
export default {
name: 'operator-button',
props : ['op'],
computed : {
operator() {
return this.$store.getters.operateurById(this.op);
}
},
}
</script>
<style scoped>
.sel{
background-color : yellow;
}
</style>
and finally my app.js look like that :
window.Vue = require('vue');
import Vuex from 'vuex';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
const store = new Vuex.Store({
state: {
periods : [],
},
mutations: {
setInitialData (state, payload) {
state.periods = payload;
},
setSelectedOperator(state, payload) {
this.getters.operateurs.forEach( op => {
op.selected = (op.id==payload.id)
})
},
},
getters : {
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
// op.selected=false; //replaced after Radu Diță answer by next line :
if (ops.selected === undefined) op.selected=false;
})
return ops;
}
},
operatorById : (state, getters) => (id) => {
return getters.operators.find(operator => operator.id==id);
},
}
});
import Chrono from './components/Chrono.vue';
var app = new Vue({
el: '#app',
store,
components : { Chrono },
mounted () {
this.$store.commit('setInitialData',
JSON.parse(this.$el.attributes.initialdata.value));
},
computed: {
...mapState(['periods']),
...mapGetters(['operators', 'operatorById']),
selectedOperator(){
this.$store.getters.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
}
},
});
Your getter in vuex for operators is always setting selected to false.
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
op.selected=false;
})
return ops;
}
}
I'm guessing you do this for initialisation, but that's a bad place to put it, as you'll never get a selected operator from that getter. Just move it to the proper mutations. setInitialData seems like the right place.
Finally I found where my problems came from :
The $el.attributes.initialdata.value came from an API and the operator objects it contained didn't have a selected property, so I added it after data was set and it was not reactive.
I just added this property on server side before converting to JSON and sending to Vue, removed the code pointed by Radu Diță since it was now useless, and it works.

How can I test a custom input Vue component

In the Vue.js documentation, there is an example of a custom input component. I'm trying to figure out how I can write a unit test for a component like that. Usage of the component would look like this
<currency-input v-model="price"></currency-input>
The full implementation can be found at https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
The documentation says
So for a component to work with v-model, it should (these can be configured in 2.2.0+):
accept a value prop
emit an input event with the new value
How do I write a unit test that ensures that I've written this component such that it will work with v-model? Ideally, I don't want to specifically test for those two conditions, I want to test the behavior that when the value changes within the component, it also changes in the model.
You can do it:
Using Vue Test Utils, and
Mounting a parent element that uses <currency-input>
Fake an input event to the inner text field of <currency-input> with a value that it transforms (13.467 is transformed by <currency-input> to 13.46)
Verify if, in the parent, the price property (bound to v-model) has changed.
Example code (using Mocha):
import { mount } from '#vue/test-utils'
import CurrencyInput from '#/components/CurrencyInput.vue'
describe('CurrencyInput.vue', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data: { price: null },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
})
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
In-browser runnable demo using Jasmine:
var CurrencyInput = 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('.') === -1 ? value.length : 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))
}
}
});
// specs code ///////////////////////////////////////////////////////////
var mount = vueTestUtils.mount;
describe('CurrencyInput', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data() { return { price: null } },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
});
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
// load jasmine htmlReporter
(function() {
var env = jasmine.getEnv()
env.addReporter(new jasmine.HtmlReporter())
env.execute()
}())
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css">
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script src="https://npmcdn.com/vue#2.5.15/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-template-compiler#2.5.15/browser.js"></script>
<script src="https://rawgit.com/vuejs/vue-test-utils/2b078c68293a41d68a0a98393f497d0b0031f41a/dist/vue-test-utils.iife.js"></script>
Note: The code above works fine (as you can see), but there can be improvements to tests involving v-model soon. Follow this issue for up-to-date info.
I would also mount a parent element that uses the component. Below a newer example with Jest and Vue Test Utils. Check the Vue documentation for more information.
import { mount } from "#vue/test-utils";
import Input from "Input.vue";
describe('Input.vue', () => {
test('changing the input element value updates the v-model', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Brendan Eich';
await wrapper.find('input').setValue(name);
expect(wrapper.vm.$data.name).toBe(name);
});
test('changing the v-model updates the input element value', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Bjarne Stroustrup';
await wrapper.setData({ name });
const inputElement = wrapper.find('input').element;
expect(inputElement.value).toBe(name);
});
});
Input.vue component:
<template>
<input :value="$attrs.value" #input="$emit('input', $event.target.value)" />
</template>

How to address the data of a component from within that component?

In a standalone Vue.js script I can mix functions and Vue data:
var vm = new Vue ({
(...)
data: {
number: 0
}
(...)
})
function return100 () {
return 100
}
vm.number = return100()
I therefore have a Vue instance (vm) which data is directly addressable via vm.<a data variable>)
How does such an addressing works in a component, since no instance of Vue is explicitly instantiated?
// the component file
<template>
(...)
</template>
<script>
function return100 () {
return 100
}
export default {
data: function () {
return {
number: 0
}
}
}
// here I would like to set number in data to what return100()
// will return
??? = return100()
</script>
You can achieve the target by using code like this.
<template>
<div>{{ name }}</div>
</template>
<script>
const vm = {
data() {
return {
name: 'hello'
};
}
};
// here you can modify the vm object
(function() {
vm.data = function() {
return {
name: 'world'
};
}
})();
export { vm as default };
</script>
But I really don't suggest you to modify data in this way and I think it could be considered as an anti-pattern in Vuejs.
In almost all the use cases I met, things could be done by using Vue's lifecycle.
For example, I prefer to write code with the style showed below.
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
data() {
return {
name: 'hello'
};
},
mounted() {
// name will be changed when this instance mounted into HTML element
const vm = this;
(function() {
vm.name = 'world';
})();
}
};
</script>