Vue: Optional chaining in templates does not seem to work

Created on 7 Feb 2020  ·  18Comments  ·  Source: vuejs/vue

Version

15.8.3

Reproduction link

https://template-explorer.vuejs.org/#%3Cdiv%20id%3D%22app%22%20v-if%3D%22obj%3F.a%22%3E%7B%7B%20msg%20%7D%7D%3C%2Fdiv%3E

Steps to reproduce

Use a v-if that uses optional chaining w/ @vue/cli version 4.2.0:

v-if="test?.length > 0"

What is expected?

no error is thrown

What is actually happening?

following error is thrown:

  Errors compiling template:

  invalid expression: Unexpected token '.' in

    test?.length > 0

  Raw expression: v-if="test?.length > 0"
contribution welcome feature request

Most helpful comment

Technically, to support such syntaxes in Vue 2, we need to:

  1. tweak the codegen to not modify such expressions (currently it converts obj?.a to obj ? .a)
  2. vue-template-es2015-compiler needs to be able to parse such syntaxes (not necessarily to transpile, to parse is enough)
  3. backport this commit to vue-loader 15 https://github.com/vuejs/vue-loader/commit/4cb447426ec19c4e07f22ce73fe134f8abaf007c#diff-c735bef98c9338c75a676df1903d2afc
  4. get ready for the possible numerous bug reports

We don't have the capacity to implement it yet. But contributions are welcome.

All 18 comments

As said in the release notes:

// Note: scripts only, support in template expressions only available in Vue 3

https://vue-next-template-explorer.netlify.com/#%7B%22src%22%3A%22%3Cdiv%20id%3D%5C%22app%5C%22%20v-if%3D%5C%22obj%3F.a%5C%22%3E%7B%7B%20msg%20%7D%7D%3C%2Fdiv%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22prefixIdentifiers%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%7D%7D

ah derp, totally read over that comment

Technically, to support such syntaxes in Vue 2, we need to:

  1. tweak the codegen to not modify such expressions (currently it converts obj?.a to obj ? .a)
  2. vue-template-es2015-compiler needs to be able to parse such syntaxes (not necessarily to transpile, to parse is enough)
  3. backport this commit to vue-loader 15 https://github.com/vuejs/vue-loader/commit/4cb447426ec19c4e07f22ce73fe134f8abaf007c#diff-c735bef98c9338c75a676df1903d2afc
  4. get ready for the possible numerous bug reports

We don't have the capacity to implement it yet. But contributions are welcome.

Potential workaround includes using lodash's get as stated in https://github.com/vuejs/vue/issues/4638#issuecomment-397770996

Another hack is to use eval.

As $eval was taken out in Vue 2, you'll have to create your own mixin so that it can be accessed in all components without having to import it into each one.

i.e.

``` This will allow $elvis to be accessed in all templates
Vue.mixin({
methods: {
$elvis: p => eval('this.'+p)
}
});

Example template


```

Although its still no substitute for the real operator, especially if you have many occurrences of it

Although its still no substitute for the real operator, especially if you have many occurrences of it

@McPo Just how safe do you think this is? I'm about to use it in production code because I do not want to set computed properties for the numerous nested fields that may be undefined or null

Although its still no substitute for the real operator, especially if you have many occurrences of it

@McPo Just how safe do you think this is? I'm about to use it in production code because I do not want to set computed properties for the numerous nested fields that may be undefined or null

As far as Im aware, it should be fine, as youre in control of the input. The only issues would be if the developer wasnt aware that its using eval and allows user input to it, but that doesn't really make sense in the case of the elvis operator.

It may have a performance impact though, in that Vue will probably recall the method on every update. I would also suspect theres some overhead in calling eval in the first place. Providing its not in a tight loop, realistically you're not likely to notice a difference.

Please do not use eval for that. It generally is bad practice and in this case specifically, it breaks in all browsers that do not support optional chaining. Whether or not you are using Babel or any other transpiler is irrelevant as they cannot transpile strings.

A better way to access properties in a fail-safe way (like with optional chaining) is the following:

<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>

The getSafe function catches any exceptions and implicitly returns undefined if the property access fails. You could also create a mixin (or use the new composition API) to reuse that function.

Please do _not_ use eval for that. It generally is bad practice and in this case specifically, it breaks in all browsers that do not support optional chaining. Whether or not you are using Babel or any other transpiler is irrelevant as they cannot transpile strings.

A better way to access properties in a fail-safe way (like with optional chaining) is the following:

<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>

The getSafe function catches any exceptions and implicitly returns undefined if the property access fails. You could also create a mixin (or use the new composition API) to reuse that function.

True I forgot to highlight the issue that it requires optional chaining to be supported in the browser. (Which isn't a major issue in my case, although I probably will stop using it anyway)

For deeply nested complex objects, this seems really important. I have objects that are five layers deep. I like the getSafe() method described by troxler, but I would prefer that chaining just work.
I mean... It wasn't "really important" to me before it became a feature in the language, but now that it's a feature, using getSafe() or other methods like that that I would use seems messy.
So, I don't mean to nag, I'm just a user sharing my priorities. Thanks for all your hard work guys.
Maybe I'll just try to prioritize the move to Vue 3.

Using the following for a TS project:

  private s<T>(obj: T | undefined): T {
    return obj || ({} as T);
  }

with VUE expressions that look like:

<q-input 
  v-model="model.value"
  label="Eyeballs"
  type="number"
  :min="s(s(model).pirate).min"
  :max="s(model).max"
/>

Gets a bit nesty beyond the first property, but the linters are happy.

private s(obj: T | undefined): T {
return obj || ({} as T);
}

@tvkit that's not exactly good implementation as it won't work with any other falsy values like empty string, zero, false. So s('').length will give me undefined instead of zero.

Using a function is outside the scope of this issue and not a viable replacement. However if someone need this kind of hack, use this :point_down:

const reducer = source => (object, property) => object?.[property] ?? undefined
const optional_chain = (...parameters) => {
    const [source, ...properties] = parameters
    return properties.reduce(reducer(source))
}
<div :foo="optional_chain({}, 'foo', 'bar', 'baz')" />

@Juraj-Masiar Good point.

@Sceat I actually really like the idea.

I guess the idea is to use optional chaining in Single File Components. But, funny enough, if you import the template from an HTML or PUG file, it works. I'd guess it shouldn't work either.

Using a function is outside the scope of this issue and not a viable replacement. However if someone need this kind of hack, use this 👇

const reducer = source => (object, property) => object?.[property] ?? undefined
const optional_chain = (...parameters) => {
  const [source, ...properties] = parameters
  return properties.reduce(reducer(source))
}
<div :foo="optional_chain({}, 'foo', 'bar', 'baz')" />

@Sceat It's not work.I fix it.

const optional_chain = (...parameters) => {
  const [source, ...properties] = parameters
  return properties.reduce((object, property) => object?.[property] ?? undefined, source)
}
const a={b:{c:{d:1}}}
console.log(optional_chain(a,'b','c','d'))

I'm on Vue 3.0.0, and it still doesn't work. Did it work on a later version?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

6pm picture 6pm  ·  3Comments

fergaldoyle picture fergaldoyle  ·  3Comments

paceband picture paceband  ·  3Comments

lmnsg picture lmnsg  ·  3Comments

robertleeplummerjr picture robertleeplummerjr  ·  3Comments