Vue.js interprets `<` and `>` in string literal as custom element - vue.js

I have a simple Vue.js app, with this template:
<h1>{{ name || '<New Document>' }}</h1>
My goal is that if name is falsy, to use the text <New Document>. This is not intended to be a custom markup tag. I want Vue.js to insert this into the document:
<h1><New Document></h1>
Instead, I get this warning on the console:
[Vue warn]: Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the "name" option.
According to the documentation, using a pair of curly brackets, {{ and }}, means text interpolation, and the text value of that expression will be used. Instead, Vue.js seems to want to treat it as HTML.
Why is that happening? How can it be resolved?

This is a great question. Like you, I assumed that everything between the curly braces would be evaluated as an expression and injected into the template. That's what the documentation implies, and in all cases I've encountered this appears to be true... except when there could be an HTML tag in a string literal. At least, that's why my experiments seem to indicate.
Take the following example:
<h1>{{ name || '<span>hello</span>' }}</h1>
There are two ways the parser could read this:
An h1 tag containing curly braces - within those, an expression we need to evaluate later.
An h1 tag followed by the string {{ name || ', then a span, then another string ' }}.
In this case, the parser is built to choose (2), and this explains why you received a compile-time error. The parser indicated that you have a tag starting with <New Document>, but it didn't have a corresponding closing tag.
If this seems like an odd design choice, consider this code:
<h1>{{'<span>hello</span>'}}</h1>
What did the user intend here? Did he/she mean to surround a span with curly braces and quotes? Probably.
As for a solution, you could manually escape the string:
{{ name || '<New Document>' }}
Alternatively, you could solve this with a computed property, which will eschew the template parser altogether:
<template>
<h1>{{ nameOrNew }}</h1>
</template>
<script>
export default {
data() {
return {
name: false,
};
},
computed: {
nameOrNew() {
return this.name || '<New Document>';
},
},
};
</script>

Vue's template parser parses HTML first (split into tags and their content) and only then parses tags attributes and texts.
For your template it will be something like:
tag: <h1>
|
+- text: "{{ name || '"
|
+- tag: <New> (attributes: ["Document"])
|
+- text: "' }}"
You should think about template as a valid HTML first, with Vue's interpolations added later.
Documentation also states that:
All Vue.js templates are valid HTML that can be parsed by spec-compliant browsers and HTML parsers.
Ref: compiler/parser/html-parser.js

you can include all possible ignored elements in this config Vue.config.ignoredElements
Vue.config.ignoredElements = ['New Document'];
i hope it helps

The reason for the behaviour isn't clear to me, but here are two possibilities if you just need it to work:
<h1 v-if="name">{{ name }}</h1>
<h1 v-else><New Document></h1>
Or, as already pointed out in another answer:
<h1>{{ name || '<New Document>' }}</h1>

Related

Vue replaces "open" attribute to value "open" in any tag

I'm using vue.js (v2.6.12) components in laravel blade templates.
For the project, I'm also using MathML in which I need to use the open attribute of <mfenced> tag to be set to some custom values. Here is the example of the math expressing in mathml.
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi>f</mi>
<mfenced close="]" open="[">
<mrow><mi>a</mi><mo>,</mo><mi>b</mi></mrow>
</mfenced>
</math>
But as soon as the page renders, the open attribute is converted into this open="open". I'm 100% sure there is no other library or script is loaded that updates like so, just plain vue. This actually breaks the math expression. So it looks like this:
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi>f</mi>
<mfenced close="]" open="open">
<mrow><mi>a</mi><mo>,</mo><mi>b</mi></mrow>
</mfenced>
</math>
Later I realized that not only in math expression, litaratily any tag, be it <div open="anything">...</div>, <span open="anything">...</span>, <custom-element open="something">...</custom-element> having open attribute behaves the same. even if I use v-pre attribute to exclude it from vue js templete compiler.
And this do not happen, as soon I disable the vue app initialization.
The question here are:
Why vue is changing the open attribute like so?
How can I stop this behaviour, to the entire page within the vue application area or at least where I choose (something like using v-pre), is there ary config or any other way around?
Why
In HTML spec there are some attributes called boolean attributes. Spec dictates what can be a value of such attribute:
If the attribute is present, its value must either be the empty string or a value that is an ASCII case-insensitive match for the attribute's canonical name, with no leading or trailing whitespace.
The values "true" and "false" are not allowed on boolean attributes. To represent a false value, the attribute has to be omitted altogether.
open is one of the boolean attributes - it is defined for the <details> element
Problem with Vue 2 is, that it treats most of the boolean attributes as global - without considering the element it is placed on. Result is that open attribute is always rendered with value "open" or removed if the value is falsy (when v-binding). This is fixed in Vue 3 as shown in 2nd example...
How
The use of v-pre is the way to go but unfortunately for you there is a bug.
See this issue. The bug was already fixed with this commit(Sep 21, 2020) but it was not released yet...
example - the "With v-pre" should work in Vue version > 2.6.12
const vm = new Vue({
el: '#app',
data() {
return {
message: 'Hi!',
html: `<div open="[" close="]">Hi from html</div>`
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.js"></script>
<div id="app">
<div open="[" close="]">{{ message }}</div>
<div v-html="html"></div>
<div v-pre>
<p open="[" close="]">With v-pre</p>
</div>
</div>
example - it works in Vue 3 - open is treated as boolean attribute only if placed on <details>
const app = Vue.createApp({
data() {
return {
message: 'This works in Vue 3!',
}
},
})
app.mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.11/vue.global.js" integrity="sha512-1gHWIGJfX0pBsPJHfyoAV4NiZ0wjjE1regXVSwglTejjna0/x/XG8tg+i3ZAsDtuci24LLxW8azhp1+VYE5daw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="app">
<div open="[" close="]">{{ message }}</div>
<details open="[">
<summary>Details</summary>
open attribute on details element is treated as boolean (renders empty value)
</details>
</div>
One workaround is to create a directive (named "attr") that sets the attribute:
Vue.directive('attr', (el, binding) => el.setAttribute(binding.arg, binding.value || ''))
Then use it in your template like v-bind but with v-attr:
<mfenced v-attr:open="'['">
Vue.directive('attr', (el, binding) => el.setAttribute(binding.arg, binding.value || ''))
new Vue({ el: '#app' })
<script src="https://unpkg.com/vue#2.6.12"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<div id="app">
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mi>f</mi>
<mfenced close="]" v-attr:open="'['">
<mrow><mi>a</mi><mo>,</mo><mi>b</mi></mrow>
</mfenced>
</math>
</div>
I've found a simple hack to solve this problem.
Why hack?
Because it is eventually going to be fixed in the comming release as pointed by #Michal, so just a quick & dirty hack is enough for now to go for it.
What I did is I placed the math content in the content and also added it to the data attribute and replacing the original content after vue has done its bad work (sorry just using blade syntax here, but it will make sense). I keep it in both places just for SEO purposes.
The template where I need math expression to be displayed.
...
<div class="proxy-content" data-proxy-content="{{ $article->content }}">
{!! $article->content !!}
</div>
...
I was using it along with jQuery, but you can easily substitute with vue.js' $el. This is what it looks in my app.js file.
...
const app = new Vue({
el: '#app',
methods: {
proxyContent() {
// Set Proxy Content.
jQuery('.proxy-content').each((i, el) => {
const $el = jQuery(el);
$el.html( jQuery('<textarea />').html( $el.data('proxy-content')).text() );
});
}
loadMathJax() {
// Load & Initialize MathJax Library.
const script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://cdn.jsdelivr.net/npm/mathjax#3/es5/tex-mml-chtml.js";
document.getElementsByTagName("head")[0].appendChild(script);
}
}
mounted(){
// Enable proxy content after mount, so we are sure no more rendering issue for templates.
this.proxyContent();
// Load MathJax library with a little delay to make sure everything is ready before loading the library.
setTimeout(() => this.loadMathJax(), 10);
}
});
...
One might argue, that I'm mixing up things outside of the scope of the vue application. For me that is not an issue, as the whole page is using vue.js and also the single thing don't make any harm even if there is another scope that is using mathml (though it depends on actual implementation).
In that case, if you want to scope it well, just use $el of vue.

Vuejs - Slots rendering nonsensical properties

I am currently having issues passing raw HTML (entity form) to a slot for syntax highlighting.
Imagine having a slot like so;
<template v-slot:code-snippet>&lbrace;&lbrace; product.title &rbrace;&rbrace;</template>
Im receiving the error
[Vue warn]: Property or method 'product' is not defined on the instance but referenced during render
which is down to &lbrace;&lbrace; product.title &rbrace;&rbrace; - remove this and it works fine.
Is there anyway I can tell the instance to stop trying to render these "properties" as it is to be interpreted as plain text?
regards
As already noted you can use v-pre. I would say that is the correct answer but it is worth noting that there are other ways this can be done. Perhaps if other constraints exist these could be useful.
Hack 1:
Move the troublesome string to a data property:
<template v-slot:code-snippet>{{ code }}</template>
data () {
return {
code: '{{ product.title }}'
}
}
Hack 2:
Wrap everything in an interpolation and throw in a bit of JS string escaping:
<template v-slot:code-snippet>{{ '\u007b\u007b product.title \u007d\u007d' }}</template>
Depending on the circumstances this can also be adapted to work with v-text or v-html as appropriate.
For anyone else needing help simply wrap the content going into the slot with <div v-pre>;
<template v-slot:code-snippet><div v-pre>{{ $component->renderCode() }}</div></template>
The div doesn't seem to be passed through to the slot.

how to bind a a variable in a vue template when really just want string interpolation

Just learning Vue2 this weekend.
I am trying to do something like this:
<a href='/arc/locations/{{location.id}}/edit'>edit here</a>
but getting an error saying:
- href="/arc/locations/{{location.id}}/edit": Interpolation inside attributes has been removed. Use v-bind or the colon shorthand instead. For example, instead of <div id="{{ val }}">, use <div :id="val">.
This is a bit confusing - I am trying to just write out a string that wont change and it seems to want to create a bound element. How woudl I just output it as a string in the url? If I can't do that, how would I just insert it into the url using Vue?
Overall, I like it but some expected gotchas.
As the warning states, you should use v-bind (of just the shorthand colon :):
<a :href="'/arc/locations/' + location.id + '/edit'">edit here</a>
Alternatively, you could make a computed property to generate the url value based off of the location.id and bind that:
computed: {
url() {
return '/arc/locations/' + this.location.id + '/edit';
}
}
<a :href="url">edit here</a>

Using vue.js in Shopify liquid templates

This should be simple but despite searching I was unable to find any solution to this. How do you use vue template tags within a liquid file? Since both Vue and liquid use the same curly brackets, I'm unable to render any of my view data:
<img src="{{ product.featured_image }}" />
results in:
<img src>
There are 36 products in my parent view component.
When I try to use custom delimiters:
new Vue({
delimiters: ['#{{', '}}'],
It won't parse with Vue:
  GET https://inkkas.com/collections/# 404 (Not Found)
UPDATE: I'm able to access Vue data with v-bind: but I still need to be able to use delimiters also.
Apparently with Shopify, you can't put these delimiters in the tag attributes at all so for those use v-bind: (the shorthand won't work). Also you have to use a single curly brace for your custom delimiter or it will still try to parse with liquid, for example:
delimiters: ['${', '}']
will work with:
<span class="title">${ product.title }</span>
Adding on a bit from where Kevin Compton left off, this is where you put the "delimiters" parameter:
const ConditionalRendering = {
data() {
return {
seen: true,
someMessage: "My message"
}
},
delimiters: ['${', '}']
}
Vue.createApp(ConditionalRendering).mount('#conditional-rendering')

How to bind to attribute in Vue JS?

I got this error
Interpolation inside attributes has been removed. Use v-bind or the
colon shorthand instead. For example, instead of <div id="{{ val }}">,
use <div :id="val">.
on this line
<a href="/Library/#Model.Username/{{myVueData.Id}}">
It works in Angular 1. How do you do it in Vue?
In your template:
<a :href="href">
And you put href in data:
new Vue({
// ...
data: {
href: 'your link'
}
})
Or use a computed property:
new Vue({
// ...
computed: {
href () {
return '/foo' + this.someValue + '/bar'
}
}
})
Just complementing ... solve the interpolation error (simple solution, I am Junior front-end developer):
Example post object in a loop:
instead of
<a href="{{post.buttonLinkExt}}">
try this way
<a v-bind:href="post.buttonLinkExt">
Use javascript code inside v-bind (or shortcut ":") :
:href="'/Library/#Model.Username' + myVueData.Id"
and
:id="'/Library/#Model.Username' + myVueData.Id"
Update Answer
Some directives can take an “argument”, denoted by a colon after the directive name. For example, the v-bind directive is used to reactively update an HTML attribute:
<a v-bind:href="url"></a>
Here href is the argument, which tells the v-bind directive to bind the element’s href attribute to the value of the expression url. You may have noticed this achieves the same result as an attribute interpolation using href="{{url}}": that is correct, and in fact, attribute interpolations are translated into v-bind bindings internally.
Found in Google this topic when searching $attrib.
Question don't specify what value is used (maybe not defined before)
For ANY parent attribute or to FILTER it, use something like that:
<template>
<component
is="div"
v-bind="$attrs"
class="bg-light-gray"
>
EXAMPLE
</component>
</template>
This instruct to create specific, dynamic and context aware, wrapper:
v-bind="$attrs" instruct to take all sended params. Not needed to declare as param object in script.
Work even with valid html attribute like class
example above mix static class with parent and join it. Use ternary operator (x=1?x:y) to choose proper one.
bonus: by "is" you can dynamically set tag like header or secion instead of div
$attrs can be binded to any tag in component so this easily enable simple transmission for one tag dynamic attributes like you define class for <input /> but wrapper and actions are added in component
Source with description: https://youtu.be/7lpemgMhi0k?t=1307
you can either use the shorthand : or v-bind
<div>
<img v-bind:src="linkAddress">
</div>
new Vue({
el: '#app',
data: {
linkAddress: 'http://i3.kym-cdn.com/photos/images/newsfeed/001/217/729/f9a.jpg'
}
});
or for when you need more than just binding an attribute you can also do:
new Vue({
el: '#app',
data: {
finishedLink: ' Google '
}
});