I have a tiny snippet that works correctly when I use :src="labels[index]", but my code is growing and I will need to use ${app.labels[index]}. I can do just ${app.labels}, but once I add the [index] part, it breaks. Can someone tell me how to add that inside the brackets?
new Vue({
el: "#app",
data: {
images: [],
labels: [],
form: {
dir: "",
fileItems: [] // An array containing objects of type: {id: number, file: File, dataUrl: [base64 encoded file string], caption: string'}
},
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
<div class="image-wrap"><img style="max-height: 430px;" :src="${app.labels[index]}" /></div>
</div>
In Vue when you use : in a prop you are basically telling Vue
Please, interpret this expression side as javascript
An example:
<super-aweosme-component :a="1 + 1" />
Will result in the prop a being 2 (because it was interpreted as javasript).
When you don't use : in a component prop you are telling Vue
Please, interpret this as plain text
An example
<super-aweosme-component a="1 + 1" />
Will result in the prop a being literally 1 + 1 (string). So you can change :src="${images.labels[index]}" to just :src="images.labels[index]" (of course, apply the changes mentioned above)
Adition
You have app.labels[index]. app doesn't exist in the template context. You have in that context images, labels and form (of course, there are attributes in that context but not important for this part)
Related
This question already has answers here:
Access vue instance/data inside filter method
(3 answers)
Closed 2 years ago.
I'm creating a simple Vuejs div component (to show a specific value) which needs to receive: a lists, a placeholder and a value as props. What I'm trying to do is displaying the value with the data from my database, if the user picks a new value from the lists, it should take that new value and display it. However, if the user never picks a new value and the data from the database is empty, it should display the placeholder.
So I have used filters to achieve this. However, it outputs an error: "Cannot read property 'lists' of undefined", which comes from the filters (I know because it outputs no error if I comment out the filters). When I changed the filter to this:
filters: {
placeholderFilter () {
return this.placeholderText || this.placeholder
}
}
It says:""Cannot read property 'placeholderText' of undefined"". So I was wondering if the filters properties executed before the data and props properties. What is the execution order of them? I have attached some of the relevant code down below. Anyway, If you could come up with a better way to achieve this. I would appreciate it!
Here is my component:
<template>
<div>{{ placeholderText | placeholderFilter }}</div>
<li #click="pickItem(index)" v-for="(list,index) in lists" :key="index">{{ list }}</li>
</template>
<script>
export default {
props: {
lists: {
type: Array,
required: true
},
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: String,
default: ''
}
},
data () {
return {
selected: -1,
placeholderText: this.value || this.placeholder
}
},
methods: {
pickItem (index) {
this.selected = index
}
},
filters: {
placeholderFilter () {
return this.lists[this.selected] || this.placeholderText || this.placeholder
}
}
}
</script>
And this is where I use it:
<my-component
placeholder="Please type something"
value="Data from database"
lists="['option1','option2','option3']"
>
</my-component>
Filters aren't bound to the component instance, so they simply don't have access to it through the this keyword. They are meant to always be passed a parameter and to return a transformed version of that parameter. So in other words, they're just methods. They were removed in Vue 3 entirely probably for that reason.
And yeah, what you're looking for here is a computed!
I'm trying to use the v-calendar component from Vuetify.
I saw in the documentation I can use the event-start prop if my events don't have the same attributes' names.
The problem is that my events have embedded attributes and I don't know if event-start handles this case.
My events :
events: [
{
id: 'b9d93291-6d95-47b9-994a-ee9f266fb6b8',
type: 'reservation_item',
attributes: {
start_date: '2020-09-23T00:00:00.000Z',
end_date: '2020-09-25T00:00:00.000Z',
},
},
]
The events example from vuetify :
events: [
{
name: 'Weekly Meeting',
start: '2020-09-07 09:00',
end: '2020-09-07 10:00',
},
],
I tried to do something like that but it doesn't work.
<v-calendar
ref="calendar"
locale="fr-fr"
:now="today"
:value="today"
:events="events"
event-start="attributes.start"
color="primary"
type="month"
></v-calendar>
After spelunking the source code for the vuetify plugin, the latter expects that the value be present in the event object, as a direct property. So you cannot acces other nested "children", it has to be a direct property.
There are two alternatives to make this work:
1- map your events array by moving the properties inside attributes to the root of your object then pass this prop to v-calendar : event-start="startDate"
2- Create a javascript class (MyEvent) with a fromJson method that take the raw JSON from your API ( this way you encapsulate the JSON into domain objects) and return an array of MyEvent instances. this way you can do for example : events[0].start and you don't even have to pass it as a value to the event-start prop, since by default it expects a start attribute as a default value.
Another advantage of this alternative, is that since the event is now encapsulated into its own javascript class, you can add helper methods, or getters/setter or any logic that would otherwise be inside your "view" logic, and contribute to have a better separation of concerns.
To make this works, I had to change my events data
<template>
<v-calendar
ref="calendar"
locale="fr-fr"
:events="myEvents"
event-start="start"
color="primary"
type="month"
></v-calendar>
</template>
data: () => ({
events: [
{
id: 'b9d93291-6d95-47b9-994a-ee9f266fb6b8',
type: 'reservation_item',
attributes: {
start_date: '2020-09-23T00:00:00.000Z',
end_date: '2020-09-25T00:00:00.000Z',
},
},
],
}),
computed: {
myEvents() {
const reservations = this.reservations
reservations.forEach((element) => {
element.start = element.attributes.start_date
element.name = 'test'
element.end = element.attributes.end_date
})
return reservations
},
}
I would like to search in a text box and filter a list of objects by whether a property matches the entered text.
Each of the objects in the filteredBuildings array creates a shape on a Google Map.
I have the following in my Vue app:
...
data: {
searchtext: '',
buildings: [
{
name: 'Home',
...
},{
name: 'Work',
...
}
],
},
computed: {
filteredBuildings () {
if( this.searchtext == '' )
return this.buildings;
const re = new RegExp( this.searchtext, 'i' );
return this.buildings.filter( b => {
// return b.name === this.searchtext;
// return b.name.toLowerCase().indexOf( this.searchtext.toLowerCase() ) > -1;
return re.test(b.name);
});
},
},
...
and the templates are complicated, but something like this:
// the main component
<div id="app">
<GoogleMapPolygon v-for="b in filteredBuildings"
:key="b.id"
:id="b.id"
:name="name"
...
/>
</div>
// the GoogleMapPolygon component
// (no <template>)
...
data: {
polygon: null,
...
},
mounted () {
this.polygon = new google.maps.Polygon({
map: map,
...
});
...
},
destroyed () {
this.polygon.setMap(null);
},
render () {
return false;
},
If I uncomment the line with === matching, it works just fine.
If I use either the indexOf matching or the RegExp .test(), I get the following error:
[Vue warn]: Error in nextTick: "NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node."
and the app completely dies.
The extra time that the RegExp or indexOf takes seems to be enough to cause the problem. That brings my thinking to nextTick(), but I'm not sure where I would put that.
The problem comes from modifying the array of buildings (the elements which are beings used in the v-for directive ) too quickly. Judging by the error, Vue is using those elements as reference points to insert other array items, but reference DOM element has disappeared by the time vue tries to insert the new one.
This could potentially be just a problem with Vue interacting with Google Maps
Solution is to wrap the component being iterated in an element which won't go away when other parts are toggled. For example (from my original question code):
<div id="app">
<section>
<GoogleMapPolygon v-for="b in filteredBuildings"
:key="b.id"
:id="b.id"
:name="name"
...
/>
</section>
</div>
I also got this problem when removing one marker and replacing it with another just after it, and this solves that issue as well:
<section>
<GoogleMapMarker
v-if="this.zoom > 15"
:key="'you-are-here-marker'"
:lat="currentLat"
:lng="currentLng"
/>
</section>
<section>
<GoogleMapMarker
v-if="this.zoom < 16"
:key="'campus-center-marker'"
:lat="campusCenter.lat"
:lng="campusCenter.lng"
/>
</section>
Before adding the section tags, I was getting the same error when these two components tried to destroy and create when this.zoom changed.
TL;DR
I am trying to dynamically build a UI from JSON. The JSON represents a vue.js app with application state (variables) & UI building logic conditional on those variables.
The JSON object of "type": "switch" (see the fiddle linked below), directs the vue.js app to display one of many "cases": {"case1": {..}, "case2": {..}} depending on the value of a state variable "variable": "key" /*translates to vueApp.key */.
Changing one of the variables (update_status) leads to DOM update initially. Changing it again after mounting the app does not affect the DOM, sadly. I'm pretty sure I am doing something stupid or missing something subtle.
Slightly longer version:
(If you're still reading this, please look at the fiddle at this point. None of the below will make sense without it. Thanks!)
Vue.js Template (with app.variables.update_status = "available")
<script type="text/x-template" id="template-switch">
<div>
<!-- Debug statements -->
Switch cases: {{data.cases}}<br>
Variables: {{$root.variables}}
<div v-for="(value, key) in data.cases">
<div v-bind:class="$root.variables[data.variable]"
v-if="key == $root.variables[data.variable]">
<all-components v-bind:data="value"></all-components>
</div>
</div>
</div>
</script>
Input JSON (bound as data in the above template):
{
// Switch on value of app.variables.update_status
"type": "switch",
"variable": "update_status", // Refers to app.variables.update_status
// Used in <script id="template-switch">
"cases": {
// if app.variables.update_status == "checking" (Initial value)
"checking": {
"type": "paragraph",
"text": "Checking for updates"
},
// if app.variables.update_status == "available" (Changed below)
"available": {
"type": "paragraph",
"text": "Updates available."
}
}
}
My question:
Assuming app is the Vue.js app, I'd expect setting app.variables.update_status = "available" should lead to DOM change. But it doesn't as described in TL;DR section. I'm hoping to understand why.
What I have tried:
Made the app watch the entire hierarchy under the object.
I initially thought this is because Vue is unable to monitor object[key] expression where key can change. But its definitely able to do it.
Last value set before mounting the app shows up. After the app is mounted, any change to the the variable sticks but doesn't trigger DOM update. (Annotated with "Doesn't work!" in the fiddle.)
Try it out!
Here's the JS Fiddle (heavily downsized, and commented for easier understanding :))
What to try:
Once the fiddle runs, open the browser console and try executing the following statements:
DEBUG.variables.update_status = "available";
DEBUG.variables.update_status = "checking";
Vue.js version: 2.5.16
Update
Also, I just found out that if I pass data object as:
new Vue({.., data: { .. , variables: {update_status: "temp"}}})
– it works!
I don’t understand this, primarily because variables field is set up to have a deep watcher. I’d assume that when it would have a its fields updated (such as variables.update_status = "new-value";), the observer would eventually trigger the DOM update. But for some reason this doesn’t happen.
I’m really hoping I’m doing something stupid, and that this isn’t this a bug.
Link to the new Fiddle that shows this behaviour: https://jsfiddle.net/g0z3xcyk/
The reason it won't update in your first fiddle is because Vue doesn't detect property addition or deletion, and you're not passing the update_status property when you instance vue, the docs explain it further.
In your second fiddle you're setting update_status when you instance vue and that's why changes, in that case, are detected.
Another option, as mentioned in the docs, is using Vue.set or recreating the object entirely by assigning it again with Object.assign
Some issues with your code:
Check Reactivity In depth as #LuisOrduz commented & answered, Vue cannot detect property addition or deletion. so two solutions:
decalare it first (as your second fiddle did), or uses Vue.set or vm.$set to add one property.
Use vm.$mount(selector) instead of using JQuery to append vm.$el; check vm.$mount
It's better to use vm.$data to access data property instead of vm[key]; check vm.$data
Below is one demo:
function registerComponents() {
Vue.component('all-components', {
template: '#template-all-components',
props: ['data']
});
Vue.component('weave-switch', {
template: '#template-switch',
props: ['data'],
methods: {
toggleStatus: function () {
this.$root.$data.variables.update_status += ' #'
}
}
});
Vue.component('paragraph', {
template: '#template-paragraph',
props: ['data']
});
}
function GenericCard(selector, options) {
var data = Object.assign({}, options.data, {variables: {}});
var watch = {};
Object.keys(data).forEach(function(key) {
watch[key] = {handler: function(val) {
}, deep: true};
});
var app = new Vue({
template: options.template,
data: function () { // uses function instead
return data
},
watch: watch
});
DEBUG = app;
return {
load: function(data) {
Object.keys(data).forEach(function(key) {
app.$data[key] = data[key];
});
//app.$data.variables.update_status = "checking"; // orginal method
app.$set(app.$data.variables, 'update_status', 'checking') // new solution
app.$mount(selector);
//var dom = app.$el;
//$(selector).append(dom); // uses vm.$mount(selector) instead
DEBUG.$set(DEBUG.$data.variables, 'update_status', 'available') // new solution
//DEBUG.$data.variables.update_status = 'available1' // or new solution
//DEBUG.variables.update_status = "available"; // orginal method
},
DEBUG: DEBUG
};
}
registerComponents();
card = GenericCard('#app', {
template: "#template-card",
data: {
ui: {}
}
});
card.load({
ui: {
// Switch on value of app.variables.update_status
"type": "switch",
"variable": "update_status", // Refers to app.variables.update_status
// Used in <script id="template-switch">
"cases": {
// if app.variables.update_status == "checking" (Initial value)
"checking": {
"type": "paragraph",
"text": "Checking for updates"
},
// if app.variables.update_status == "available" (Changed below)
"available": {
"type": "paragraph",
"text": "Updates available."
}
}
}
});
Vue.config.productionTip = false
function toggleStatus() {
card.DEBUG.$data.variables.update_status += ' #'
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script type="text/x-template" id="template-all-components">
<div v-if="data.type == 'paragraph'">
<paragraph v-bind:data="data.text"></paragraph>
</div>
<div v-else-if="data.type == 'switch'">
<weave-switch v-bind:data="data"></weave-switch>
</div>
</script>
<script type="text/x-template" id="template-switch">
<div>
<!-- Debug statements -->
Switch cases: {{data.cases}}<br>
Variables: {{$root.variables}}
<button #click="toggleStatus()">Toggle</button>
<div v-for="(value, key) in data.cases">
<div v-bind:class="$root.$data.variables[data.variable]"
v-if="key == $root.$data.variables[data.variable]">
<all-components v-bind:data="value"></all-components>
</div>
</div>
</div>
</script>
<script type="text/x-template" id="template-paragraph">
<p>{{data}}</p>
</script>
<script type="text/x-template" id="template-card">
<all-components v-bind:data="ui"></all-components>
</script>
<div id="app">
</div>
<button onclick="toggleStatus()">Toggle2</button>
Why I can't bind the object properties in Vue? The object addr is not reactive immediately, but test is reactive, how come? In this case, how should I bind it?
HTML
<div id="app">
<input type="text" id="contactNum" v-model="addr.contactNum" name="contactNum">
<input type="text" id="test" v-model="test" name="test">
<br/>
{{addr}}<br/>
{{addr.contactNum}}<br/>
{{test}}
</div>
Javascript
var vm = new Vue({
el: '#app',
data: {
addr: {},
test: ""
}
});
Jsfiddle
During initialisation Vue sets up getters and setters for every known property. Since contactNum isn't initially set up, Vue doesn't know about that property and can not update it properly. This can be easly fixed by adding contactNum to your addr object.
var vm = new Vue({
el: '#app',
data: {
addr: {
contactNum: "" // <-- this one
},
test: ""
}
});
The above is called reactivity in Vue. Since Vue doesn't support adding properties dynamically to its reactivity system, we may need some kind of workaround. A possible solution is provided by the API. In case of dynamically added properties we can use Vue.set(vm.someObject, 'b', 2).
Doing so the markup would need to get some update. Instead of using v-model it'd be better to use an event listener like #input. In this case our markup could look like this.
<input type="text" id="contactNum" #input="update(addr, 'contactNum', $event)" name="contactNum">
So basically the function will get triggered every time the input elements value changes. Obviously doing so will also require some adjustments on the JS part.
var vm = new Vue({
el: '#app',
data: {
addr: {},
test: ""
},
methods: {
update: function(obj, prop, event) {
Vue.set(obj, prop, event.target.value);
}
}
});
Since Vue triggers Vue.set() on any reactive element, we simply call it on our own because Vue doesn't recognizes a dynamically added property as a reactive one. Of course, this is only one possible solution and there may be lots of other workarounds. A fully working example can be seen here.
As per my comments, there are several things that you want to consider:
The reason why your code is not working is due to the inherent inability of JS to watch for changes in object properties. This means that even though addr is reactive, any properties added to addr that is not done when it is declared will make it non-reactive. Refer to the VueJS docs for more details: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
If you are going to have an arbitrary number of input fields, you are probably better of composing a custom input component, and simply use v-for to iteratively inject input fields based on the number of input fields you have.
Now back to the second point, if you know what fields addr will have, you can simply declare it in your app. We create a new updateFormData method, which is called by the component:
data: {
addrFields: ['contactNum', ...],
addr: {},
test: ""
},
methods: {
updateFormData: function(id, value) {
this.$set(this.addr, id, value);
}
}
We can still store your form data in the addr object, which will be updated by the updateFormData method based on the received payload using .$set(). Now, we can then create a custom Vue component for your input element.
In the example below, the component will iterate through all your addrFields, and pass down the addrField as a prop using :id="addrField". We also want to make sure that we capture the custom-named updated event emitted from within the component.
<my-input
v-for="(addrField, i) in addrFields"
:key="i"
:id="addrField"
v-on:inputUpdated="updateFormData"></my-input>
The template can look something like the following. It simply uses the id prop for both its id, name, and placeholder attribute (the latter for easy identification in the demo). We bind the #change and #input events, forcing it to trigger the updated callback:
<script type="text/template" id="my-input">
<input
type="text"
:id="id"
:name="id"
:placeholder="id"
#input="updated"
#change="updated">
</script>
In the component logic, you let it know that it will receive id as a prop, and that it should emit an inputUpdated event using $.emit(). We attach the ID and value as payloads, so that we can inform the parent what has updated:
var myInput = Vue.component('my-input', {
template: '#my-input',
props: {
id: {
type: String
}
},
methods: {
updated: function() {
this.$emit('inputUpdated', this.id, this.$el.value);
}
}
});
With the code above, we have a working example. In this case, I have created an arbirary array of input fields: contactNum, a, b, and c:
var myInput = Vue.component('my-input', {
template: '#my-input',
props: {
id: {
type: String
}
},
methods: {
updated: function() {
this.$emit('updated', this.id, this.$el.value);
}
}
});
var vm = new Vue({
el: '#app',
data: {
addrFields: ['contactNum', 'a', 'b', 'c'],
addr: {},
test: ""
},
methods: {
updateFormData: function(id, value) {
this.$set(this.addr, id, value);
}
}
});
<script src="https://unpkg.com/vue#2.1.3/dist/vue.js"></script>
<div id="app">
<my-input
v-for="(addrField, i) in addrFields"
:key="i"
:id="addrField"
v-on:updated="updateFormData"></my-input>
<input type="text" id="test" v-model="test" name="test" placeholder="test">
<br/>
<strong>addr:</strong> {{addr}}<br/>
<strong>addr.contactNum:</strong> {{addr.contactNum}}<br />
<strong>test:</strong> {{test}}
</div>
<script type="text/template" id="my-input">
<input
type="text"
:id="id"
:name="id"
:placeholder="id"
#input="updated"
#change="updated">
</script>
Edit your Vue data with this since it's getter and setter methods are not set up. Also, check out Declarative Reactive Rendering on Vue docs here:
data: {
addr: {
contactNum: "" // <-- this one
},
test: ""
}