I'm struggling to develop a simple component and use it inside a loop:
<template id="measurement">
<tr class="d-flex">
</tr>
</template>
Vue.component('measurement', {
template: '#measurement',
props: {
name: String,
data: Object,
val: String,
},
});
This is obviously not functional yet but already fails:
<table v-for="(m, idx) in sortedMeters">
<measurement v-bind:data="m"></measurement>
</table>
gives ReferenceError: Can't find variable: m inside view. For a strange reason the same thing works, i.e. without error, in a paragraph:
<p v-for="(m, idx) in sortedMeters">
<measurement v-bind:data="m"></measurement>
</p>
What causes the variable to be not found?
PS.: here's a fiddle: https://jsfiddle.net/andig2/u47gh3w1/. It shows a different error as soon as the table is included.
Update It is intended that the loop produces multiple tables. Rows per table will be created by multiple measurements
TLDR: Before Vue is passed the DOM template, the browser is hoisting <measurement v-bind:name="i" v-bind:data="m"> outside the <table> (outside v-for context), leading to the errors in Vue. This is a known caveat of DOM template parsing.
The HTML spec requires the <table> contain only specific child elements:
<caption>
<colgroup>
<thead>
<tbody>
<tr>
<tfoot>
<script> or <template> intermixed with above
Similarly, the content model of <tr> is:
<td>
<th>
<script> or <template> intermixed with above
The DOM parser of compliant browsers automatically hoists disallowed elements – such as <measurement> – outside the table. This happens before the scripting stage (before Vue even gets to see it).
For instance, this markup:
<table>
<tr v-for="(m,i) in obj">
<measurement v-bind:name="i" v-bind:data="m"></measurement>
</tr>
</table>
...becomes this after DOM parsing (before any scripting):
<measurement v-bind:name="i" v-bind:data="m"></measurement> <!-- hoisted outside v-for -->
<table>
<tr v-for="(m,i) in obj">
</tr>
</table>
Notice how i and m are then outside the context of the v-for loop, which results in Vue runtime errors about i and m not defined (unless by chance your component coincidentally declared them already). m was intended to be bound to <measurement>'s data prop, but since that failed, data is simply its initial value (also undefined), causing the rendering of {{data.value}} to fail with Error in render: "TypeError: Cannot read property 'value' of undefined".
To demonstrate hoisting without these runtime errors and without Vue, run the code snippet below:
<table style="border: solid green">
<tr>
<div>1. hoisted outside</div>
<td>3. inside table</td>
2. also hoisted outside
</tr>
</table>
...then inspect the result in your browser's DevTools, which should look like this:
<div>1. hoisted outside</div>
2. also hoisted outside
<table style="border: solid green">
<tr>
<td>3. inside table</td>
</tr>
</table>
Solution 1: Use <tr is="measurement">
If you prefer DOM templates, you could use the is attribute on a <tr> to specify measurement as the type (as suggested by the Vue docs and by another answer). This first requires the <measurement> template use <td> or <th> as a container element inside <tr> to be valid HTML:
<template id="measurement">
<tr>
<td>{{name}} -> {{data.value}}</td>
</tr>
</template>
<div id="app">
<table v-for="(m,i) in sortedMeters">
<tr is="measurement" v-bind:name="i" v-bind:data="m" v-bind:key="i"></tr>
</table>
</div>
Vue.component('measurement', {
template: '#measurement',
props: {
name: String,
data: Object
}
})
new Vue({
el: '#app',
data: {
sortedMeters: {
apple: {value: 100},
banana: {value: 200}
},
}
})
<script src="https://unpkg.com/vue#2.6.11"></script>
<template id="measurement">
<tr>
<td>{{name}} -> {{data.value}}</td>
</tr>
</template>
<div id="app">
<table v-for="(m,i) in sortedMeters">
<tr is="measurement" v-bind:name="i" v-bind:data="m" v-bind:key="i"></tr>
</table>
</div>
Solution 2: Wrap <table> in component
If you prefer DOM templates, you could use a wrapper component for <table>, which would be able to contain <measurement> without the hoisting caveat.
Vue.component('my-table', {
template: `<table><slot/></table>`
})
<div id="app">
<my-table v-for="(m, i) in sortedMeters">
<measurement v-bind:name="i" v-bind:data="m"></measurement>
</my-table>
</div>
Vue.component('measurement', {
template: '#measurement',
props: {
name: String,
data: Object
}
})
Vue.component('my-table', {
template: `<table><slot/></table>`
})
new Vue({
el: '#app',
data: {
sortedMeters: {
apple: {value: 100},
banana: {value: 200}
},
}
})
<script src="https://unpkg.com/vue#2.6.11"></script>
<template id="measurement">
<tr>
<td>{{name}} -> {{data.value}}</td>
</tr>
</template>
<div id="app">
<my-table v-for="(m, i) in sortedMeters">
<measurement v-bind:name="i" v-bind:data="m"></measurement>
</my-table>
</div>
Solution 3: Move <table> markup into template string
You could move the entire <table> into a component's template string, where the DOM template caveats could be avoided. Similarly, you could move the <table> into a single file component, but I assume you have a significant need for DOM templates instead.
Vue.component('my-table', {
template: `<div>
<table v-for="(m, idx) in sortedMeters">
<measurement v-bind:data="m"></measurement>
</table>
</div>`,
props: {
sortedMeters: Object
}
})
<div id="app">
<my-table v-bind:sorted-meters="sortedMeters"></my-table>
</div>
Vue.component('measurement', {
template: '#measurement',
props: {
name: String,
data: Object
}
})
Vue.component('my-table', {
template: `<div>
<table v-for="(m,i) in sortedMeters">
<measurement v-bind:name="i" v-bind:data="m" v-bind:key="i"></measurement>
</table>
</div>`,
props: {
sortedMeters: Object
}
})
new Vue({
el: '#app',
data: {
sortedMeters: {
apple: {value: 100},
banana: {value: 200}
},
}
})
<script src="https://unpkg.com/vue#2.6.11"></script>
<template id="measurement">
<tr>
<td>{{name}} -> {{data.value}}</td>
</tr>
</template>
<div id="app">
<my-table :sorted-meters="sortedMeters"></my-table>
</div>
If you replace
<table v-for="(m, idx) in sortedMeters">
<measurement v-bind:data="m"></measurement>
</table>
with
<template v-for="(m, idx) in sortedMeters">
<table>
<measurement v-bind:data="m"></measurement>
</table>
</template>
You'll end up with working code.
But you'll most likely want to use
<table>
<template v-for="(m, idx) in sortedMeters">
<measurement v-bind:data="m"></measurement>
</template>
</table>
or
<table>
<measurement v-for="(m, idx) in sortedMeters" v-bind:data="m"></measurement>
</table>
It's because you've missing <td> inside <tr>. Without it your component produces invalid html markup and extracts "slot" data outside <tr> causing error.
Your template should looks like:
<template id="measurement">
<tr>
<td>{{name}} -> {{data.value}}</td>
</tr>
</template>
You also need to move v-for to measurement:
<table border="1">
<tr is="measurement" v-for="(m,index) in obj" :key="index" v-bind:name="index" v-bind:data="m"></measurement>
</table>
You can use is attribute to set component name.
Working fiddle: https://jsfiddle.net/cyaj0ukh/
Related
i am new in VueJs and i am having that little problem, first here is my sample HTML code :
<div class="search">
<input :class="{ longwidth : isActive }" v-show="showInput" type="text">
<img #click="hideImgShowInput" v-show="hideImg" src="Assets/images/search-icon.png">
</div>
i have followed the documentation exactly, and i am using PHPStorm as editor, but my function that changes the 'isActive' variable is not working i am having this error:
Attribute :class is not allowed here
Any help would be much appreciated.
That sounds like a PHPStorm warning. Ignore it, or try a Vue-aware editor like vs code or atom. Your code looks fine to me.
This error causes the component to not render
My case is similar to the following
<script type="text/x-template" id="game-row">
<tr>
<td>{{ title }}</td>
...
</tr>
</script>
This way when adding the class attribute to tr element, causes the mentioned message
<script type="text/x-template" id="game-row">
<tr :class="{ active: isActive }">
<td>{{ title }}</td>
...
</tr>
</script>
The way to fix it was to pass the class attribute in the custom component call, as follows
<script type="text/x-template" id="game">
<div>
<p v-if="isLoading">Loading...</p>
<template v-else>
<table>
<caption>Game {{ title }}</caption>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr
is="game-day-row"
v-for="game of games"
:class="{ 'active': isActive }" <!-- Here I set the class -->
:key="game.id"
:game="game"
></tr>
</tbody>
</table>
</template>
</div>
</script>
You can read about it in the vue class and style guide
https://v2.vuejs.org/v2/guide/class-and-style.html#With-Components
In the html output the class active is added to the tr element
I have a Vue template that displays a set of tracks, but my site isn't loading. Console says
Property or method "track" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
(found in root instance)
I'm sure the issue has to do with parent-child vue instance relationship or something along those lines, but I'm not too sure, since I'm only starting to learn about vue.js
I read over this https://v2.vuejs.org/v2/guide/components.html#Using-v-on-with-Custom-Events, but I'm having a hard time seeing what the problem is. What's going wrong?
Here's the template in html:
<div id="vue-div">
<template v-for="track in spotifyResults.items">
<spotify :track="track"></spotify>
</template>
<!-- spotify vue template -->
<script type="text/x-template" id="spotifyResult">
<tr>
<td>
<img :src="track.album.images[2].url"/>
</td>
<td>${track.artists[0].name}
</td>
<td>${track.album.name}
</td>
<td>${track.name}
</td>
<td>
<a :href="track.uri">Play</a>
</td>
<td><span v-on:click="add_track_to_library(track.album.images[2].url, track.artists[0].name, track.album.name, track.name, spotify, track.uri, none)">+</span></td>
</tr>
</script>
</div>
And here is the js:
var spotify = {
template: '#spotifyResult',
delimiters: ['${', '}'],
props: ['track']
}
self.vue = new Vue({
el: "#vue-div",
delimiters: ['${', '}'],
unsafeDelimiters: ['!{', '}'],
components: {
spotify: spotify,
},
data: {
spotifyResults: {
items: []
},
}
});
You are defining your component template incorrectly. Vue is attempting to render your #spotifyResult template as a <template> block.
To use X-Templates, you use
<script type="text/x-template" id="spotifyResult">
<tr>
<!-- etc -->
</tr>
</script>
This <script> tag needs to go in your main .html file, outside the root element.
JSFiddle Demo ~ https://jsfiddle.net/7u0aboe6/3/
I am trying to create a Custom Element that allows me to collapse itself from a simple click delegate, but it doesn't seem to work.
I have this code in my js file
import {inject, bindable, bindingMode} from 'aurelia-framework';
export class DataGridCustomElement {
#bindable({ defaultBindingMode: bindingMode.oneTime }) columns = [];
#bindable({ defaultBindingMode: bindingMode.oneTime }) items = [];
#bindable() collpased = true;
collapseClick() {
this.collapsed = !this.collpased;
}
}
And here is my HTML file
<template>
<require from='./data-grid.css'></require>
<div class="collapse-arrow" click.delegate="collapseClick()">
<span class="collapse-icon glyphicon ${collapsed ? 'glyphicon-plus' : 'glyphicon-minus'}" aria-hidden="true"></span>
<span>Order Lines</span>
</div>
<div class="collapse-block" css="${collapsed ? 'display:none;' : 'display:block;'}">
<table class="data-grid">
<thead>
<tr>
<td repeat.for="column of columns">
${column.title}
</td>
</tr>
</thead>
<tbody>
<tr repeat.for="item of items">
<td repeat.for="column of columns">
${item[column.propertyName]}
</td>
</tr>
</tbody>
</table>
</div>
</template>
The crazy thing is it just doesn't seem to at all. It shows collapsed as being false from the get go, even though I set it to true in the class.
I am calling it like so
<data-grid columns.bind="invoiceColumns" items.bind="lineData"></data-grid>
Any ideas? Am I missing something about Custom Elements?
Easy solution. You have a typo in this.collapsed = !this.collpased;.
Let me setup the question with a simple case.
I have an HTML table, the rows of which are controlled by an observableArray. It works great.
If the observableArray has zero elements in it however, I want a single row to say so. I tried this markup, which "kind of" works:
<tbody data-bind="if: $root.data.contacts().length == 0">
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
<tbody data-bind="foreach: $root.data.contacts">
SNIP - a tbody with the rows is here when elements > zero
</tbody>
When I say "kind of", I mean VISIBLY. It really does show up at zero elements and really does go away at > zero elements like what you would expect. However when you open the DOM inspector (dev tools) and look at the DOM in memory, you find that there are TWO tbody sections, not one. Now one tbody is always empty of course, but two tbody tags is not HTML5 correct, so this must be fixed this is not the desired markup.
Being a Knockout newbie, I tried to fix this problem with a virtual element:
<!-- ko if: $root.data.contacts().length == 0 -->
<tbody>
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
<!-- /ko -->
Unfortunately this doesn't work for our build process: we minify HTML prior to compression and comments get eliminated.
I was under the impression that KO bindings applied to the CONTAINER ELEMENT ITSELF as well as descendants, but this seems to not be so. Is there a way to tell KO to apply to container elements as well as children, or do I need to change the markup in some way OTHER THAN a virtual container?
Like you, my first choice would be virtual tags for an if binding. But since that's not an option, how about swappable templates?
var vm = {
contacts: ko.observableArray()
};
ko.applyBindings(vm);
setTimeout(function() {
vm.contacts(['One', 'Two', 'Three']);
}, 2500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<template id="empty-body">
<tbody>
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
</template>
<template id="normal-body">
<tbody data-bind="foreach: contacts">
<tr>
<td data-bind="text:$data"></td>
</tr>
</tbody>
</template>
<table data-bind="template: contacts().length === 0 ? 'empty-body' : 'normal-body'"></table>
The Knockout-Repeat binding applies the binding to the element itself. It does so by using a node preprocessor to wrap elements with the repeat binding in virtual (comment-based) elements at run time.
var vm = {
contacts: ko.observableArray()
};
ko.applyBindings(vm);
setTimeout(function() {
vm.contacts(['One', 'Two', 'Three']);
}, 2500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
<script src="https://rawgit.com/mbest/knockout-repeat/master/knockout-repeat.js"></script>
<table>
<tbody data-bind="repeat: !contacts().length && 1">
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
<tbody data-bind="repeat: contacts().length && 1" data-repeat-bind="foreach: contacts">
<tr>
<td data-bind="text:$data"></td>
</tr>
</tbody>
</table>
I am trying to build a grid element, but am having some issues with a content insertion points. Here's my element with content insertion points:
<template>
<content select="[data-guerilla-grid-service]"></content>
<table id="guerillaGrid">
<thead>
<tr>
<template is="dom-repeat" items="{{columns}}" as="column">
<th style$="{{getColumnStyle(column)}}" data-sort-field$="{{column.sortField}}" on-click="sortClick"><span>{{column.header}}</span></th>
</template>
</tr>
</thead>
<content select="[data-guerilla-grid-items]"></content>
</table>
And here's where I'm using it:
<template>
<guerilla-grid id="feedbackGrid">
<muted-feedback-service id="service" items="{{items}}" data-guerilla-grid-service on-load-finished="serviceLoadFinished" sort-field="Version"></muted-feedback-service>
<tbody data-guerilla-grid-items>
<template is="dom-repeat" items="{{items}}">
<tr>
<td>{{item.formattedTimestamp}}</td>
<td>{{item.hash}}</td>
<td>{{item.version}}</td>
<td>{{item.serialNumber}}</td>
</tr>
</template>
</tbody>
</guerilla-grid>
Am I doing this right?
Content selector does not appear to work with tag. Switched to a doing a table with divs using css display attribute table options.