I'm currently working on a chat program written in VueJS and have a problem when trying to display the message with a given timestamp, because I need to loop inside an object containing two arrays (message array and timestamp array)
My data object looks like this:
messages: [ //saving text-messages of each chatroom in order (0 => chatroom1, 1 => chatroom2, ..)
{ //chatroom1 with static data for testing
text: ["first message chatroom1", "second message chatroom1"],
time: ["08:38", "09:02"]
},
{ //chatroom2
text: [],
time: []
},
{ //chatroom3
text: [],
time: []
}
]
When I would try to loop only through the text[] Array, my code would look like this:
<p class="message-class message-send" v-for="messages in messages[values.num].text">{{ messages }}</p>
giving me back
<p class="message-class message-send">first message chatroom1</p> //messages[0].text[0]
<p class="message-class message-send">second message chatroom2</p> //messages[0].text[1]
The [values.num] is the number of the selected chatroom passed in order to select the specific data (0 for chatroom1, ...)
When trying to iterate through text and time in one single loop (surely I want the time to be displayed beyond every single text message and not after all messages) I thought something similar to this should do the work:
<div v-for="messages in messages[values.num]">
<p class="message-class message-send">{{ messages.text }}</p>
<span class="message-time-send">{{ messages.time }}</span>
</div>
The problem in this case is that I display the whole text and time array without iterating through the arrays itself.
Trying something like
<p class="message-class message-send">{{ messages.text[some number] }}</p>
also doesn't work.
Is there anyone who can help me solving this problem? Or should I consider trying to build the messages object in a different way
This should work.
<div v-for="(message, index) in messages[values.num].text">
<p class="message-class message-send">{{ message}}</p>
<span class="message-time-send">{{ messages[values.num].time[index] }}</span>
</div>
Generally, a structure like this would be preferrable:
messages: [
[ // room 1
{
"time": "08:38",
"text": "first message chatroom1"
},
{
"time": "09:02",
"text": "second message chatroom1"
}
],
...
]
Because logically, timestamp and message text are both properties of one single "chatroom message" item. But if you can't or do not want to change your structure, you can get the index in a v-for statement like this: v-for="(message, index) in messages[values.num].text" (see the official docs). Then you can use that same index to access the corresponding item in the time array:
<div v-for="(message, index) in messages[values.num].text">
<p class="message-class message-send">{{ message}}</p>
<span class="message-time-send">{{ messages[values.num].time[index] }}</span>
</div>
Related
I am retrieving a list of data from an api and need to fill the specific <select></select> tags, which is associated to a few radio button, with some of the data as <options></options>. The radio buttons waiting for an event (#change/#click) and executing and axios get request. Everthing works fine. I click on a radio button and retrieving the data as response (vue tools also showing the right data) but the <option></option> tags are not updating. Now when I click on another radio button, I am getting again the right data from the api BUT now the <option></option> tags are refreshing with the data from the previous response.
Template
<!-- CREATING 7 RADIO BUTTONS FOR THE CURRENT WEEK FROM MON-SUN -->
<div class="wrapper" v-for="item in inputDetails">
<input :id="'datetime[0]['+item.labelText+']'" type="radio" name="datetime[0][date]" v-model="formData.datetime[0].date" :value="item.inputValue" #change="getTimes" />
</div>
<!-- CREATING THE TIME PICKER -->
<select id="datetime[0][time]" name="datetime[0][time]" v-model="formData.datetime[0].time">
<option selected="selected"></option>
<option v-for="item in selectOptionTimes[0]" :value="item.value">{{ item.label }}</option>
</select>
<!--
2 MORE RADIO BUTTON SECTION AND TIME PICKER SECTIONS WITH DIFFERENT INDEXES
<input id="datetime[1][time]"...
-->
Script
data() {
return {
formData: {
datetime: [
{date: '', time: ''},
{date: '', time: ''},
{date: '', time: ''},
]
}
selectOptionTimes: [],
}
},
methods: {
getTimes: function (current) {
let instanceIndex = current.currentTarget.id.match(/(?<=\[)([0-9])(?=])/g)[0]; // getting the index of the current datetime section
axios.get('/api-url', {
params: {
location_id: this.formData.location_id,
date: current.currentTarget.value
}
}).then(response => {
this.selectOptionTimes[instanceIndex] = response.data;
});
}
}
Does someone know what the problem is here?
You cannot assign a value to an arbitrary index within an empty Array in this way. You must either completely replace the Array with values that hydrate that index, or you must use $set.
So, to recap:
BAD
this.selectOptionTimes[instanceIndex] = response.data
GOOD
this.$set(this.selectOptionTimes, instanceIndex, response.data)
Note though, that this has an unintended consequence. If you have an empty array, and call this.$set on an index greater than 0, the array will be filled with empty values up to your index.
What might make more sense is using an {} instead along with this.$set and looping over the Object.keys instead of the array directly.
Fiddle showing $set on index with an empty array
Fiddle showing Object usage instead
I have an array of objects called users and every object has a property called username
let users = [
{
username: 'Test1'
},
{
username: 'Test12'
}
]
I also have an array called onlineUsers which also contains objects with a property called username:
let onlineUsers = [
{
username: 'Test1'
}
]
Right now I'm looping over users and displaying the users like this:
<p v-for='member in users'>
<span class='username'>{{ member.username }}</span>
</p>
And now I'm trying to figure out how to apply a special class to the users which are also present in both the users and onlineUsers array. The problem is I can't figure out how. I tried double v-for but that didn't seem to work.
You can use :class and .some() array helper function like:
:class is used for conditional binding
.some() returns true if at least one condition is true
VUE :
<p v-for='member in users'>
<span class='username' :class="{special : onlineUsers.some(v=>v.username === member.username) }">{{ member.username }}</span>
</p>
CSS :
.special: {
...
}
Reference :
https://v2.vuejs.org/v2/guide/class-and-style.html
array_some_mdn_reference
I am accessing an online API and want to use the text value to populate a ngb-typeahead dropdown. There is a working example on the Angular Bootstrap website using Wikipedia, but the returned data from the Wikipedia API is different to the data I am getting from a geocoding API. The data I get is returned in this format:
{
"suggestions": [
{
"text": "23 Queen Charlotte Drive, Aotea, Porirua, Wellington, 5024, NZL",
"magicKey": "dHA9MCNsb2M9NDMwNzcyNzQjbG5nPTMzI2huPTIzI2xicz0xMDk6NDg1NDQwMzU=",
"isCollection": false
},
{
"text": "23 Queen Mary Avenue, Epsom, Auckland, 1023, NZL",
"magicKey": "dHA9MCNsb2M9NDMwNDY4MjUjbG5nPTMzI2ZhPTE0NDE3OTIjaG49MjMjbGJzPTEwOTo0ODU0NDMyNA==",
"isCollection": false
},
I have been trying to access text in response data with the following:
return this.http
.get<any>(GIS_URL, {params: GIS_PARAMS.set('text', term)}).pipe(
map(response => response.suggestions)
);
I have also read the Angular tutorial here on dealing with response data, but the difference in the example is that they are getting an array of Hero's whereas I am getting an object containing an array of suggestions.
The typeahead looks like:
HTML
<fieldset class="form-inline">
<div class="form-group">
<label for="typeahead-http">Search for a wiki page:</label>
<input id="typeahead-http" type="text" class="form-control mx-sm-3" [class.is-invalid]="searchFailed" [(ngModel)]="model" [ngbTypeahead]="search" placeholder="Wikipedia search" />
<small *ngIf="searching" class="form-text text-muted">searching...</small>
<div class="invalid-feedback" *ngIf="searchFailed">Sorry, suggestions could not be loaded.</div>
</div>
</fieldset>
<hr>
<pre>Model: {{ model | json }}</pre>
Full code on StackBlitz is here.
I am new to Angular, so a verbose answer would be great.
You need to specify resultFormatter and inputFormatter on the typeahead input (refer to Typeahead).
Explanation
Your search method in the service returns a list of suggestion Objects which each look like:
{
isCollection: ...
magicKey: ...
text: ...
}
However by default the typeahead control expects a list of strings, hence it displays your objects as [Object object].
You need to tell the typeahead control how to determine a string value from your object, you do this via resultFormatter and inputFormatter.
These inputs take a function, which has the object as an input and the string display value as its output.
formatter below is that function, it will be called for each item displayed in the list. If you expand it to a normal function you can put a breakpoint in it and see it being called in this manner.
Solution
<input id="typeahead-http" ... [inputFormatter]="formatter" [resultFormatter]="formatter"/>
TypeScript file:
formatter = (item:any) => item.text as string;
Updated StackBlitz
https://stackblitz.com/edit/so-typeahead?file=src%2Fapp%2Ftypeahead-http.ts
Follow-up questions
item in the formatter:
Consider:
formatter = (item:any) => item.text as string;
is shorthand for:
function format(item: any){
return item.text as string;
}
They typeahead control/directive iterates the items returned by search(..) and calls this method which each one. The results are displayed in the select list.
map(response => response.suggestions)
The response from the service is an object like:
{ // object
suggestions:
[
{ ..., text: 'Place 1' },
{ ..., text: 'Place 2' }
]
}
That is an object containing a list named suggestions. The typeahead expects a list only, so the map transforms the object containing list => list only.
Does the formatter that you have defined do both input and result?
Yes, as it is assigned to both [inputFormatter] and [resultFormatter] in the template.
Alternative answer
The mapping is done entirely in the service:
return this.http
.get<any>(GIS_URL, {params: GIS_PARAMS.set('text', term)}).pipe(
map(response => response.suggestions.map(suggestion => suggestion.text)),
);
Each response object is mapped to the list of suggestions. Each suggestion is mapped (using JavaScript map) to its text value.
You can use this solution provided you don't need access to any of the other suggestion properties outside of the service.
I'm trying to save the contents of a form into a hierarchical data structure:
$scope.milestones = [
{milestoneName: "milestone 1",
id:"milestoneOne",
headingID:"headingOne",
panelClass:"in",
tasks:[
{
taskSubject:"Get cost agreement confirmation",
category:"#7FFF00",
dueDate:"July 5, 2015",
repeat: true,
assignee:"Jiman Ilitad",
estHours:"3"},
{
taskSubject:"Get cost agreement confirmation",
category:"#7FFF00",
dueDate:"July 5, 2015",
repeat: true,
assignee:"Jiman Ilitad",
estHours:"3"}
]}
];
I'm using ng-repeat to display milestones and then within each of the milestones I have tasks. Each milestone can have one or more tasks added to it via a basic html form where each input corresponds with a value in the push.
Here is the script which defines the push:
$scope.addTask = function(index){
$scope.milestones.tasks.push({
taskSubject: $scope.index.formTaskSubject,
category: $scope.index.formCategory,
dueDate: $scope.index.formDate,
repeat: $scope.index.formRepeat,
assignee: $scope.index.formAssignee,
estHours: $scope.index.formEstTime
})
};
I'm currently getting an error: TypeError: Cannot read property 'push' of undefined
This error means that you're trying to call the method push on something that doesn't exist. Since $scope.milestones is an array, you need to specify which element in that array you're trying to add a task to.
Based on your plunker, you just need to call addTask() with an additional parameter specifying the index of the milestone you wish to modify.
In your ng-click, pass in an index to the milestone. For example, change:
<a class="btn btn-primary" ng-click="addTask()">Save</a>
To:
<a class="btn btn-primary" ng-click="addTask($index)">Save</a>
The above assumes that $index is the index to your $scope.milestones array, which is assigned by ng-repeat="milestone in milestones". It can easily change if you nest ng-repeats, breaking your code.
To avoid this, just pass the milestone object itself directly into addTask.
In your HTML:
<div ... ng-repeat="milestone in milestones" ...>
...
<a ... ng-click="addTaskTo(milestone)" ...>Save</a>
...
In your controller:
$scope.addTaskTo = function(milestone) {
milestone.tasks.push(...);
Below is my JSON object which I would like to display the name in both the parent and child array.
$scope.result= [
{
"id": 1,
"name": "1002",
"parentArray": [
{
"id": 28,
"name": "PRODP1",
"shortCode": "PRODP1"
}
]
}
I want to display Name:1002 Parent_Name:PRODP1
I tried {{item.name}} which will only display 1002.But I need to display the name of parentArray as well.
Since the parentArray is also an array your going to need a nested ng-repeat.
If this is a large page then this may cause a performance issue.
<div ng-repeat="item in result">
{{item.name}}
<div ng-repeat="innerItem in item.parentArray">
{{innerItem.name}}
</div>
</div>
parentArray is an...array, so you need to access it using an index:
<div ng-repeat="item in result">
Name: {{ item.name }} Parent_Name: {{ item.parentArray.length ? item.parentArray[0].name : '' }}
</div>
That's under the assumption that there is one object in parentArray. You might need to iterate it, or you might need to check to see if it exists depending on your requirements.