I'm trying to set a class on items of which the value of a two-dimensional array is set to true. The array looks like this:
test[item][day]
And it get created and set in a method.
The template:
<div>
<div v-for="item in items" class="item-row">
<div class="item-name">item: {{ item.id }}</div>
<div v-for="day in days" class="item-header" v-bind:class="{'activeDay' : test[item.id][day]}">{{ day }}</div>
</div>
</div>
This 'works', but I do get an error:
vue.js?3de6:2902 TypeError: Cannot read property '0' of undefined(…)
This is because the array gets filled in too late I guess? I set my array like: test[1][2] = 'true', but read somewhere I should be using Vue.set. However, I can't seem to get the right syntax to use it with a two-dimensional array.
Any advise on what the proper way is to achieve this?
As the issue seems to relate to the content not being populated when the component is rendered it is just a matter of controlling either when the template renders this 2d array, or when you populate its content.
option 1
If you have access to all the data the 2d array needs when the component is first created (i.e. no ajax requests for data going on). Then simply move the call to the method that builds the array into your components created method, i.e.
created () {
this.buildItems()
},
...
option 2
If you need to request the data via ajax or for some other reason can not populate the array in the created method then just set a param to indicate when it is built and control its rendering, i.e.
# template
<div v-if="itemsReady" v-for="item in items" class="item-row">
# script
data () {
return {
itemsReady: false, // set to true when you have built the items array
...
},
},
methods: {
buildItems () {
... // logic to build array
this.itemsReady = true
},
...
},
Related
I am using vue3 and wonder how to pass data the correct way.
My Component structure is one table (items loaded via pinia store): XTableComponent
The XTableComponent has a child: XModalComponent. In the rendered table I have a button in each row. #click on that stores the current item in a data item
XTableComponent:
<template>
...that mentioned table in each line a button with #click and the item in the iteration as param
<x-model-component v-if="currentItem" :item="currentItem ref="x-modal"></x-modal-component>
</template>
<script>
export default {
data: () => {
return {
currentItem: {},
itemListStore: useItemListStore()
}
},
computed: {
itemList() {
return this.itemListStore.list
}
methods: {
showModal(item){
this.currentItem = item
this.$refs['x-modal'].show()
}
}
}
</script>
My Child component looks a bit like this:
XModalComponent:
<template>
....
<input v-model:value="innerItem.something" type="text">
<button #click="save">save</button>
</template>
<script>
export default {
props: {
item: Object
},
data: () => {
return {
innerItem: {}
}
}
mounted() {
this.innerItem = item
},
methods: {
save() {
console.log(this.innerItem) //this does not show the manipulated value of `something`
}
}
}
</script>
Now, if I manipulate the input in my child component, and trigger a click event, the value does not get changed on my data item ...
what did I get wrong in vue3 with reactiveness, proxeis and passing props?
p.s. my code is kind of pseude code here, so please be fair with me on typos, or obvious parts
that are missing
p.p.s. I am used to vue2 quite well, so maybe I mix concepts. please tell me that too.
p.p.p.s. my table renders correctly, the modal looks fine. i double checked all names and typos.
So, as we figured out, the problem came from the way innerItem.something was bound to the input, and some confusion around the v-model directive.
As a recap, the v-model directive is short-hand for setting a prop on a component and listening to an event which updates the value.
In Vue 2, that was:
<child-component
:value="myValue"
#input="(nevValue) => myValue = newValue"
/>
which is equivalent to
<child-component v-model="myValue"/>
and it allows a variable to be changed by parent as well as child ("two-way binding"). Note that property name and event matches that of a HTML input element (the "value" attribute and the "input" event), probably because it represents the most familiar case, where a value is bound to an input:
<input type="text" v-model="myText"/>
However, to allow for multiple two-way bindings on a component, Vue 2 also introduced a second way, which allows to bind to any of the child components props, not just "value". This is the .sync modifier:
<child-component :childComponentProp.sync="myVar"/>
which is equivalent to:
<child-component
:childComponentProp="myVar"
#update:childComponentProp="(newValue) => myVar = newValue"
/>
In Vue 3, they decided to unify the two, dropping .sync and instead allowing to pass a prop name to v-model similar to how slot names are passed to the v-slot directive, i.e. v-model:childComponentProp="myVar", and similar as v-slot alone is equivalent to v-slot:default, v-model alone is equivalent to v-model:modelValue. So it is equivalent to:
<my-component
:modelValue="myValue"
#update:modelValue="(nevValue) => myValue = newValue"
/>
But the above only applies for Vue components. When using v-model on an HTML input element, it sill behaves like in Vue 2 and binds to the "value" attribute and the "input" event. It is still equivalent to:
<input :value="myValue" #input="(nevValue) => myValue = nevValue"/>
However, that behavior is a special case of plain v-model (i.e. without a prop name). And I think this is where the confusion comes from.
Using v-model:value explicitly binds to the #update:value event, i.e. this
<input v-model:value="innerItem.something" type="text">
is equivalent to:
<input type="text"
:value="innerItem.something"
#update:value="(newValue) => innerItem.something = newValue"
/>
but that event is not sent by a plain HTML element.
So long long story short, you have to use v-model= instead of v-model:value= when binding to a native input element.
Does that make sense? Hope it helps.
I have a simple <firebase-query> tag, and I'd like to manipulate some of the data before having it displayed through a <dom-repeat>. For example, I need to turn some fields into links, and also parse some dates.
So, I need to get the data once it's ready, loop through each item, and change some of the values.
To do that, I have an observer on the data to detect when it's ready. However, I can't figure out how to loop through the data from that JavaScript function. For some reason, for(var i in items) doesn't work, although the items do exist.
Here is the component:
<dom-module id="cool-stuff">
<template>
<firebase-query id="query" path="/items" data="{{items}}"></firebase-query>
<template is="dom-repeat" items="{{items}}" as="item">
[[item.name]]<br />
[[item.date]]<br />
</template>
</template>
<script>
Polymer({
is: 'ix-table',
properties: {
items: {type: Object, observer: "_itemsChanged"},
}
itemsChanged: function(data) {
// how do I loop through the data received from firebase-query?
console.log(data);
}
});
</script>
</dom-module>
Ideally, all I'd want to do in the observer function is something like:
for(var i in data) {
obj = data[i];
obj.name = '<a href="/item/"+obj.key>'+ojb.name+'</a>';
}
But I can't seem to be able to loop through the data.
Inside the observer function, console.log(data) returns some weird stuff like this:
Array[o]
0: Object (which contains a proper item)
1: Object (same)
2: Object (same)
Update:
Here is a screenshot of what console.log(data) returns (from inside the observer):
The array seems to be populated with all the objects, but it shows as Array[0]. So it won't let me loop through them.
Update 2:
Thanks to arfost here is the solution:
<script>
Polymer({
is: 'ix-table',
properties: {
items: {type: Object},
}
observers: [
'_itemsChanged(items.splices)'
],
_itemsChanged: function(changeRecord) {
if (changeRecord) {
changeRecord.indexSplices.forEach(function(s) {
for (var i=0; i<s.addedCount; i++) {
var index = s.index + i;
var item = s.object[index];
console.log('Item ' + item.name + ' added at index ' + index);
// do whatever needed with the item here:
this.items[index].name = "New name";
}
}, this);
}
},
});
</script>
<firebase-query> results
Note that <firebase-query> results in an array of objects. Let's say your database contained the following items under /notes/<USER_ID>/:
Your <firebase-query> would look similar to this:
<firebase-query
id="query"
app-name="notes"
path="/notes/[[user.uid]]"
data="{{notes}}">
</firebase-query>
(where user is bound to <firebase-auth>.user).
Assuming the user is logged in, <firebase-query> would then populate its data property (i.e., bound to notes) with the following array:
Note how each object contains a $key property, which corresponds to the item's key seen in the Firebase console's Database view.
You could then iterate notes directly with <dom-repeat>:
<template is="dom-repeat" items="[[notes]]">
<li>
<div>key: [[item.$key]]</div>
<div>body: [[item.body]]</div>
<div>title: [[item.title]]</div>
</li>
</template>
Binding to HTML strings
You should be aware that the string data bindings are rendered literally in this case, so attempting to set name to obj.name = '<a href="...">' would render the literal string instead of an anchor. Instead, you should declare the tags in your template, and bind the key and name properties inside those tags. So, your observer could be replaced with this:
<template is="dom-repeat" items="{{items}}" as="item">
<a href$="/item/[[item.key]]">[[item.name]]</a><br />
[[item.date]]<br />
</template>
Iterating an array
The following note is only relevant if you prefer to mutate the data before displaying it...
When iterating an array, you should avoid for..in because it doesn't guarantee order of iteration, and because it may iterate over enumerable properties you might not necessarily care about. Instead, you could use for..of (assuming ES6 is available to your app):
for (let note of notes) {
note.title += ' ...';
}
or Array.prototype.forEach():
notes.forEach(function(note) {
note.title += ' ...';
});
I thinks I have run into the same issue as you.
It come from the way firebase query is getting the array, the way polymer obersvers works, and is hidden by the fact that the javascript console is reference based when it show the objects.
In fact what really happen here, is that firebase query is creating an empty array, which trigger your polymer observer.
So your function is called as soon as the array is created, but still empty and you can't iterate through, since it's empty. You then log it, where the primitives sub-properties are correctly displayed (array[0])
Then firebase begin to populate the array with the datas. The arrays reference stay the same, so polymer won't fire the observer again, and in the console, when it try to display the array it display the array referenced in the log, which now contains the datas.
I recommend that you use a array mutation observer in place of your simple one as follow
`properties: {
items: {type: Object},
},
,
observers: [
'_itemsChanged(items.splices)'
],`
It will fire every time an object is added to your array, and you would be able to do the work you need :)
I had the link for the documentation on array mutation observer :)
polymer array mutation observer
I hope this will solve your issue,
have a good day.
i don't think i can think of a scenario where you'd need to mutate the data by looping through the array rather than just using computed bindings. like this:
<template is="dom-repeat" items="{{items}}" as="item">
<child-el date="{{_computeDate(item.date)}}"></child-el><br />
<child-el attr1="{{_someOtherConversion(item.prop1)}}"></child-el><br />
<child-el attr2="{{_iPromiseAnyConversionCanBeDoneLikeThis(item.prop2)}}"></child-el><br />
</template>
<script>
_computeDate: function(item) {
//do date converstion
}
I'm trying to create a dynamic slider that can be reused with different images.
<div id="sly_container" class="sly">
<ul>
{{#each propertyImages}}
<li><img src="{{ImageURL}}"></li>
{{/each}}
</ul>
<button id="gallery_btn-prev"><img class="gallery_arrow"/>prev</button>
<span id="middleSpan"></span>
<button id="gallery_btn-next"><img class="gallery_arrow"/>next</button>
</div>
I populate propertyImages with an httpRequest (I make several of these):
(function(o){
HTTP.call( 'GET', 'https://api.rentcafe.com/rentcafeapi.aspx?requestType=images&type=propertyImages&companyCode=c00000084939&propertyCode='+v+'', {},
function( error, response ) {
if ( error ) {
console.log( error );
} else {
var content = JSON.parse(response["content"]);
obj[p] = content;
if( o == l){
CommunityImages.insert(obj)
Session.set('imagesLoaded',true);
}
}
console.log(v+ ' ' + p + ' ' + o + ' images delivered')
})
}(o++))
and then use this helper:
Template.sly.helpers({
propertyImages: function(){
if(Session.get('property') && CommunityImages.find().fetch()[0]){
return CommunityImages.find().fetch()[0][Session.get('property')]
}
})
After it renders I run some more logic on it to create the slider from the images. It works well when there is one slider per view, since it is dependent on the Session.set('property', 'whatever') but I want to have many on the same page each populated with different images. I can add keys and values to the image objects, so I suppose maybe I can do this with a Spacebars conditional? In the end I'd like to have something like this
<div id="summit-sly">{{> sly}}</div>
<div id="hillcroft-sly">{{> sly}}</div>
<div id="merrit_station-sly">{{> sly}}</div>
with each slider containing it's respective images, OR I'm now seeing that maybe partials could work somehow:
{{>sly summit}}
{{>sly hillcroft}}
{{>sly merrit_station}}
Each slider will basically need it's own class name, so that the logic that runs on render will target each one specifically and not all of them.
Indeed, you can use partials in Blaze spacebars with either:
A single argument that acts as the data context of the called template (as exemplified in Meteor Guide > View > User Interfaces > UI components > Smart components).
Named arguments (as shown in Meteor Guide > View > Blaze > Spacebars > Template inclusion), which are actually assembled into a data object (see below).
{{> subComponent arg1="value-of-arg1" arg2=helperThatReturnsValueOfArg2}}
Another tutorial: http://meteorcapture.com/spacebars/#template-inclusion-ex-2
Reference: Meteor API Docs > Packages > spacebars > Inclusion and Block Arguments
Inclusion tags ({{> foo}}) and block tags ({{#foo}}) take a single data argument, or no argument. Any other form of arguments will be interpreted as an object specification or a nested helper:
Object specification: If there are only keyword arguments, as in {{#with x=1 y=2}} or {{> prettyBox color=red}}, the keyword arguments will be assembled into a data object with properties named after the keywords.
Nested Helper: If there is a positional argument followed by other (positional or keyword arguments), the first argument is called on the others using the normal helper argument calling convention.
Then you retrieve the data context through the templateInstance.data property:
In an onCreated, onRendered or onDestroyed callback, the template instance is directly available in this.
In a helper or in the HTML part of the template, the data context is directly available in this (no need to look for its data child property). In a helper, you can also access the template through Template.instance().
In an event handler, the template instance is passed as the 2nd argument of the listener.
I'm struggling with an issue that I will explain giving a simple demo.
There's following very simple document in People Collection.
{
"_id" : "vxmLRndHgNocZouJg",
"fname" : "John" ,
"nicks" : [ "Johnny" , "Jo"]
}
Now let's consider following templates. Basically I display username and a list of nicknames with input field for adding more nicknames.
<head>
<title>test</title>
</head>
<body>
{{> name}}<br/>
{{> nicks}}
</body>
<template name="name">
<input type="text" value="{{fname}}"/>
</template>
<template name="nicks">
{{#each nicks}}
<div>{{this}}</div>
{{else}}
no nicks yet
{{/each}}
<input type="text" name="nicks"/>
<input type="submit"/>
</template>
My client javascript code is as follows:
Template.name.fname = function() {
return People.findOne({"fname" : "John"},{
transform : function(doc) {
return doc.fname;
}
});
}
Template.name.rendered = function() {
console.log('Template "name" rendered!');
}
Template.nicks.nicks = function() {
var john = People.findOne({"fname" : "John"});
if(john) return john.nicks;
}
Template.nicks.events({
'click input[type="submit"]' : function () {
var johnId = People.findOne({"fname" : "John"})._id; // demo code
People.update(johnId,{
$addToSet : {
nicks : $('input[name="nicks"]').val()
}
})
}
});
My problem is that after adding nickname (update of nicks field in a document) template name is re-rendered (I know because I console.log it). When I query People collection to provide data for name template I use transform option so changes in nicks field shouldn't have impact on name template.
Meteor docs supports this:
Cursors are a reactive data source. The first time you retrieve a cursor's documents with fetch, map, or forEach inside a reactive computation (eg, a template or autorun), Meteor will register a dependency on the underlying data. Any change to the collection that changes the documents in a cursor will trigger a recomputation.
Why template name is re-rendered then?
The template is re-rendered because you change the People collection.
When you alter the People collection, Meteor automatically assumes that everything that it provides data to needs to be recalculated. (Which your name template does via Template.name.fname.
Even though you transform the output of the cursor, the People collection has changed in some way. The query is done before the transform is used, in other words, its not the transform that is looked at but the whole collection.
Meteor thinks that perhaps your document with {'fname':'John'} may have some other field that might have changed and it needs to requery it to check (which the nicks field has been altered). The transform is then applied after the requery.
Your HTML might not actually change at this point, only if the cursor returns something different will the html be changed.
If it becomes an issue in any scenario (i.e forms being cleared or DOM being changed when it shouldn't be) you can use the {{#isolate}} {{/isolate}} blocks to ensure that only everything inside them is re-rendered and nothing outside.
Is there any way to get the object of the current iteration in Handlebars?
code:
<script id="HandleBarTemplate1" type="text/x-handlebars-template">
{{#each objArr}}
<img src="{{objField1}}"/>
<strong>Name:</strong> {{objField2}}
<input type="button" onclick="processObject({{.}});"/>
{{/each}}
</script>
I've mentioned processObject({{.}}) That is incorrect. That's where I need a replacement/Solution.Hope you get what I'm tryin' to say.
The contents of objArr might look like
var objArr = [{objField1:"smith.jpg",objField2:"Smith"},{objField1:"jane.jpg",objField2:"Jane"},...]
Template compilation code is:
var source = document.getElementById("HandleBarTemplate1").innerHTML;
var compiledTemplate = Handlebars.compile(source);
var html = compiledTemplate({objArr:objArr});
If I could get the reference to the Object, then It's so easy to process the data. Rather than passing a field to the function and searching through entire array to get the required object and then process it.
I Prefer a solution without a custom block helper/custom expression helper but if none exists, I'd rather go for a custom block helper. Any solution without searching through the entire array is welcome!
I would suggest a different route.
Knowing that you already have a reference to objArr, make a global or name-spaced variable pointing to it.
For example: window.objArr = objArr;
Create your click handler that does whatever you wish:
function processObject(key){
}
call that with your key inside your template:
< script id="HandleBarTemplate1" type="text/x-handlebars-template">
{{#each objArr}}
<img src="{{objField1}}"/>
<strong>Name:</strong> {{objField2}}
<input type="button" onclick="processObject({{objField2}});"/>
{{/each}}
</script>
Other alternatives:
Customer Handler.
Other alternatives:
If you can't create a reference to objArray, you might could store the properties of the object within data- attributes if you're using html5. processObject could then retrieve them.
I do this with a Handlebars helper and an array of context objects.
Somewhere we have an array of context objects, which will store a reference to each context object we need to reference:
var ContextObjects = [];
We need to register a Handlebars helper function that stores the current context object in the array for us when we place "{{obj}}" in the template. The helper returns the index of the object in the array, which is what gets rendered:
// Must be function and not lambda to preserve the correct 'this'
Handlebars.registerHelper("obj", function ()
{
var contextObject = this; // the current context object
var index = ContextObjects.indexOf(contextObject);
if (index < 0)
{
index = ContextObjects.length;
ContextObjects[index] = contextObject;
}
return index;
});
Next we need a function to retrieve the object from the index on the element:
// Get the context object for the current event (e.g. click or context).
function GetContextObject(event)
{
var $e = $(event.currentTarget);
var index = $e.attr("data-obj");
return ContextObjects[index];
}
We tie them together in the template something like this:
{{#each Items}}
<div data-obj="{{obj}}" onclick="DoSomething(GetContextObject(event));">...</div>
{{/each}}
Which may render something like this:
<div data-obj="0" onclick="DoSomething(GetContextObject(event));">...</div>
<div data-obj="1" onclick="DoSomething(GetContextObject(event));">...</div>
<div data-obj="2" onclick="DoSomething(GetContextObject(event));">...</div>
...