I am looking for a little bit of help with logically fitting two objects with common reference into AngularJs ngRepeat.
Example objects (these get called from a service):
$scope.objArr1 = [
{ id: 1, Name: 'Name 1', Value: 'Value 1', },
{ id: 2, Name: 'Name 3', Value: 'Value 2', },
{ id: 3, Name: 'Name 3', Value: 'Value 3', },
];
$scope.objArr2 = [
{ id: 1, Name: 'Name 1', Value: 'Value 1', ObjArr1: { id: 1, Name: 'Name 1', Value: 'Value 1', }, },
{ id: 2, Name: 'Name 1', Value: 'Value 1', ObjArr1: { id: 1, Name: 'Name 1', Value: 'Value 1', }, },
{ id: 3, Name: 'Name 1', Value: 'Value 1', ObjArr1: { id: 3, Name: 'Name 3', Value: 'Value 3', }, },
];
Something along those lines. Basically if you can think of it this way; first array objects form buckets while second array objects form items that fit into corresponding bucket.
First approach
HTML:
<ul>
<li data-ng-repeat="item in objArr1 | filter : someFilter">{{item.Name}}
<ul>
<!-- how to filter objArr2 items based on objArr1 property ? -->
<li data-ng-repeat="item2 in objArr2 | filter : someOtherFilter">{{item2.Name}}</li>
</ul>
</li>
</ul>
In simple terms I was trying to filter $scope.objArr2 items that correspond to the current repeater item in the inner repeater. I tried various things with someOtherFilter but I was unable to reference the item from outer repeater.
Problem
I couldn't figure out how get this filtering bit to work.
Second approach
When all else failed I decided to combine the data structures into one like so:
// deep copy to avoid dependency
angular.copy($scope.objArr1, $scope.objArr3);
// loop over objArr3 and add empty array objArr2
// which we will populate a bit later
angular.forEach($scope.objArr3, function (val, key) {
$scope.objArr3[key]["objArr2"] = [];
});
Then I setup a $watch-er`to monitor both objArr1 and objArr2 because I don't know when these will return.
$scope.$watchGroup(['objArr1', 'objArr2'], function (newVals, oldVals) {
// check to make sure there is stuff to loop over
// i am wrongly assuming there will be items in both objArr1 and objArr2
// i'll worry about what to do when there is no data a bit later
if(newVals[0].length > 0 && newVals[1].length > 0) {
angular.forEach($scope.objArr1, function (val1, key1) {
angular.forEach($scope.objArr2, function (val2, key2) {
if (val1.Id === val2.objArr1.Id) {
$scope.objArr3[key1].objArr2.push(val2);
}
});
});
}
});
HTML:
<ul>
<li data-ng-repeat="item in objArr1 | filter : someFilter">{{item.Name}}
<ul>
<li data-ng-repeat="item2 in item.objArr2">{{item2.Name}}</li>
</ul>
</li>
</ul>
Problem
While this has worked just fine on the surface I get a lovely Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting! in the console.
I am a bit puzzled what would cause for $digest to fire so many times.
However, by commenting update line $scope.objArr3[key1].objArr2.push(val2); of my watcher the error goes away. But then I don't understand how this would result in extra digest iterations.
Halp
In the end either of the approach that I came up with has some problem. While second approach actually does its job and populates my repeater correctly but there is that nasty error in the console.
Anyone with a bit more experience in this field please help.
Update
Some of the silly things I tried with someOtheFilter are:
data-ng-repeat="item2 in objArr2 | filter : someOtherFilter"
$scope.someOtherFilter = function(item){
// item is always the current inner repeaters item2 object
// that just the way angular filter works
return item.objArr2 === $scope.objArr1.Id; // this is silly but idea is there
};
data-ng-repeat="item2 in objArr2 | filter : someOtherFilter(item)"
$scope.someOtherFilter = function(item){
// if memory serves me right
// in this case item is always repeaters current item2 object
// with no ability to reference outer repeaters current item object
}
data-ng-repeat="item2 in objArr2 | filter : someOtherFilter(item, item2)"
$scope.someOtherFilter = function(item, item2) {
// if memory serves me right
// in this case item was always inner repeaters current item2 object
// and item2 is always undefined
// again with no ability to reference outer repeaters current item
}
At this point I gave up on first approach. But thinking about it now I might have been able to utilise $index (if inner repeater somehow or other didn't overwrite outer repeaters $index reference) to get index value of the outer repeater and try to get at $scope.objArr1[index].
No matter which scenario would have worked for someOtherFilter inner working only need to compare inner object objArr1.Id to outer objects Id.
UPDATE (learn from my mistakes)
OK, after confirming the answer as working I still had the same issue in my production example Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!.
After cooling down for a few days I decided to revisit this problem and this is what I found.
<div class="block" data-ng-repeat="team in Teams | filter : validateAgeCategory">
<div data-ng-style="getHeaderStyle()">
<span>{{team.Name}}</span>
<!-- bunch of things removed for brevity -->
</div>
<ul data-ng-style="getListStyle()">
<li data-ng-repeat="players in Players | filter : { Team: { Id: team.Id, }, }">
<a data-ng-style="getListItemStyle()" data-ng-href="#/players/{{player.Id}}">{{player.Name}}</a>
</li>
</ul>
</div>
I adapted my Team/Player example for easier understanding. Regardless notice that in production I use a few ng-style calls to retrieve CSS.
Reason why I am doing so is because IE has a tendency to remove {{}} definitions from inline style="color: {{color}};" definition during document load. It's IE bug so to speak.
Moving on, what I found is that the innermost ng-style was causing the error with $digest. By removing data-ng-style="getListItemStyle()" everything is happy. Everything bu me of course.
Looking at this as an overhead it would be better to create CSS classes and instead apply classes based on some indexing to style my HTML.
There you have it.
OK, I'll try my best to help.
I think the problem with your second approach is somehow related to this question. Read the comments there. It might be related to the list being changed by the filter.
As for your first approach, I'm still not sure what you were trying to do, but I've created this example to show you that you can filter inside nested ngRepeats.
BTW, If you need to access outer $index inside an inner ngRepeat, you can use ngInit.
Related
hello everyone i have array of objects, and inside each object i have also array of objects..
i did the v-for inside the v-for to display data
at first i wanted for each row to show the data of the first element of the each child of the parent array and onClick event, i wanted to change the data only in the specifique row.
infos: [{
name: 'name1',
infosName: [{
place: 'place.1.1',
surface: '100'
},
{
place: 'place.1.2',
surface: '200'
}
]
},
{
name: 'name2',
infosName: [{
place: 'place.2.1',
surface: '300'
},
{
place: 'place.2.2',
surface: '400'
}
]
}
]
i created a method to display the data and got on parametres Two indexs
this a jsfiddle to understand more the problem
Thank you
https://jsfiddle.net/f0ehwacm/2
There are several issues needing fixing, but you are on the right lines.
Most importantly, you need to store not just one "myIndex" but a separate "myIndex" for each row
That is the root cause of your problem.
Let me rephrase your question?
I believe you are hoping for four buttons. The top two buttons choose between two options.
Completely separately, the bottom two buttons choose between two options.
It would be easier for readers to understand your intention if you called the two top buttons "Question 1, Option A", and "Question 1, Option B". And then the bottom two "Question 2...". Then they would understand why when you click on one of the buttons, you want to affect the output of the table for that row only.
Avoid using generic terms like "index" and "i"
These make it unnecessarily difficult for people to understand what you intend. Better to use a concrete noun, in this case "question" or "answer", and prefix it with "i" when you mean the index, such as "iQuestion" for the index of the question and "question" for the question itself.
You seem to have a single function "getInfos" which does BOTH getting and setting of information
This is a major problem. You should separate the two functions.
When you click, you want to run a "set" function, that updates your index.
When you are simply displaying, you can access a "get" function, which does not change anything.
You need to store an index for each row
In my terminology, you need to store the index of your answer to each question.
So instead of this.myIndex starting at 0, you have it starting at [0,0]. Each of the two values can be updated separately, allowing the program to update the answer to one row (i.e. one question), while leaving the other row unchanged.
I have renamed this variable to this.myAnswer to make it easier to understand.
this.$set when writing to an array that you want Vue to react to
I initially wrote the "setAnswer" function as follows:
this.myAnswer[iQuestion]=iAnswer
However, I found that the on-screen display was not updating. This is a common problem in Vue, when you update not the main property listed in data(), but an array element of that property.
This is because Vue is not tracking the updates of the array elements, only the array itself. So if you were to reassign the entire array, Vue would notice.
The workaround is to tell Vue explicitly that you are updating something that needs to be reactive. Vue will then update it on screen.
To do this, change your assignment from this format:
this.array[index] = value
To this
this.$set(this.array, index, value)
Vue provides this function this.$set, which executes your normal this.array[index] = value and tells Vue to do the screen update.
How to cope with missing "infosName"
In response to your question in the comments. You have a convenient place to solve this: your getAnswer() function.
Change from this:
getAnswer(iQuestion,iAnswer){
return {
'name' : this.infos[iQuestion].infosName[iAnswer].place,
'surface' : this.infos[iQuestion].infosName[iAnswer].surface
}
to this:
getAnswer(iQuestion,iAnswer){
if (this.infos.length>iQuestion &&
this.infos[iQuestion].infosName &&
this.infos[iQuestion].infosName.length>iAnswer
){
return {
'name' : this.infos[iQuestion].infosName[iAnswer].place,
'surface' : this.infos[iQuestion].infosName[iAnswer].surface
}
else return {
name : "",
surface: ""
}
}
Solution
html:
<div id="app">
<div v-for="(question,iQuestion) in infos">
<div class="row d-flex">
<span style="margin-right:10px" v-for="(answer,iAnswer) in question.infosName" class="badge badge-primary" #click="setAnswer(iQuestion,iAnswer)"><i class="fa fa-eye" style="margin-right:10px;cursor: pointer"></i>{{ answer.place }}</span> </div>
<div class="row">
<p>Name : {{ getAnswer(iQuestion,myAnswer[iQuestion]).name }} </p>
<p>Surface : {{ getAnswer(iQuestion,myAnswer[iQuestion]).surface }}</p>
</div>
</div>
</div>
JS:
new Vue({
el :'#app',
data : function(){
return {
myAnswer : [0,0],
infos : [
{
name : 'name1',
infosName : [
{
place : 'Question 1, Option A',
surface : '100'
},
{
place : 'Question 2, Option B',
surface : '200'
}
]
},
{
name : 'name2',
infosName : [
{
place : 'Question 2, Option A',
surface : '300'
},
{
place : 'Question 2, Option B',
surface : '400'
}
]
}
]
}
},
methods:{
setAnswer(iQuestion,iAnswer){
this.$set(this.myAnswer,iQuestion,iAnswer)
},
getAnswer(iQuestion,iAnswer){
return {
'name' : this.infos[iQuestion].infosName[iAnswer].place,
'surface' : this.infos[iQuestion].infosName[iAnswer].surface
}
}
}
})
I have an element in my View in Aurelia that is not getting updated when an object from its Viewmodel is getting updated. I've seen the documentation about Pub/Sub and Event Aggregators, however this seems a little heavy-handed for what I want to do, since I am not trying to communicate between two different resources, but rather just within a View and its Viewmodel.
When a change occurs to the object in the Viewmodel, I don't know how to correctly update (or trigger an update to) the string interpolation in the View.
My code is as follows
myview.html
<h1>My List</h1>
<ul>
<li repeat.for="group of modelObject.groups">
<span>${group.id}</span>
<span repeat.for="state of group.region">${state}</span>
</li>
<ul>
<button click.delegate(editModelObject())>Edit</button>
myviewmodel.js
constructor()
{
this.modelObject = {
<other stuff>,
"groups": [
{
"id": "default",
"regions" : ["NY", "CT", "NJ"]
},
{
"id": "west",
"regions" : ["CA", "OR"]
}
],
<more stuff>
}
}
editModelObject() {
<something that updates both the id and regions of a group in this.modelObject>
}
For some reason, the states are correctly changing in the view, but the id's are not. Do I need to use something like Pub/Sub to get the two-way binding to work correctly? Or is there a simple thing that I am missing or doing wrong?
This works if you change a property of one of the array's objects. But this doesn't work if you assign one of the array's index because this would require dirty-checking. See https://github.com/aurelia/binding/issues/64
To solve your problem you should use splice() instead of indexed assignment. For instance:
const newItem = { id: 77, name: 'Test 77', obj: { name: 'Sub Name 77' } };
//instead of this.model.items[0] = newItem; use below
this.model.items.splice(0, 1, newItem);
Running example https://gist.run/?id=087bc928de6532784eaf834eb918cffa
I have an array of objects:
[{
name: "test",
type: 0,
speed: 50
}, {
name: "test2",
type: 4,
speed: 10
}, {
name: "test3",
type: 4,
speed: 67
}, {
name: "test4",
type: 2,
speed: 40
}]
I want to show the array with ng-repeat..
Until here everything is simple...
But now I want to add few buttons, each button will filter the array with other parameters...
For example:
Button one: Show only the objects where the speed is more than X,
Button two: Show only the objects where the type is Y..
etc...
I now how to use angular filters, but I couldn't understand how to use it for my needs...
Thank you very much!
The most suitable solution for your purpose it is using filter named filter. It can accept custom filtering function in argument.
HTML
<div ng-repeat="item in items|filter:filterItem"></div>
Then you should define function filterItem in your controller:
JS
// assume that first button sets `$scope.hideSlow` flag and the second `$scope.showType` respectively
$scope.filterItem = function(item) {
return (!$scope.hideSlow || item.speed > X) &&
(!$scope.showType || item.type === Y);
}
You can add extra filtering logic into this function, but do not forget write tests on it to be sure that your function filtering properly
I might suggest creating a custom filter that inspects the model to determine the result set out of the filter. Bind the buttons to methods on the scope to check from within the filter.
https://docs.angularjs.org/tutorial/step_09
In lunr.js, you can add a unique reference using the .ref() method but I can't find any method to add extra data/info about that particular record. Is it not possible or am I missing something really obvious.
I even tried assigning an object to ref but it saves it as a string.
EDIT
For now I am saving all the contents as a JSON string in .ref(), which works but is really ugly to use.
lunr does not store the documents that you pass it to index at all, the way it indexes means that the original document is not available to lunr at all, so there is no way of passing and storing meta data associated with the indexed object.
A better solution is to keep your records outside of lunr, and use the reference you give to lunr to pull out the record when you get the search results. That way you can store whatever arbitrary meta data you want.
A simple implementation might look like this, its overly simplistic but you get the idea...
var documents = [{
id: 1,
title: "Third rock from the sun",
album: "Are you expirienced",
rating: 8
},{
id: 2,
title: "If 6 Was 9",
album: "Axis bold as love",
rating: 7
},{
id: 3,
title: "1983...(A Merman I Should Turn to Be)",
album: "Electric Ladyland",
rating: 10
}]
var db = documents.reduce(function (acc, document) {
acc[document.id] = document
return acc
}, {})
var idx = lunr(function () {
this.ref('id')
this.field('title', { boost: 10 })
this.field('album')
})
documents.forEach(function (document) {
idx.add(document)
})
var results = idx.search("love").forEach(function (result) {
return db[result.ref]
})
I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Link to the problematic page: http://www.kangarooelectronics.com/fakeDB/#/tables/edit/2
My issue is that when I go to remove a column I get:
Object # has no method 'deleteRecord'
As I understand this is due to the object I'm iterating through having no references to the controller because of the way I am constructing the array that I use to create my list.
Removing tables works fine, which are listed in the following fashion:
{{#each model itemController='TableList'}}
<a {{action removeTable this}}>Delete</a>
{{/each}}
I'm iterating through the columns via:
{{#each column in currentColumns itemController='TablesEdit'}}
<a {{action removeColumn column}}>Drop</a>
{{/each}}
Snippet from FIXTURES object:
FakeDB.Table.FIXTURES = [
{
id: 1,
name: 'Users',
columns: {
1:{'colId':1, 'name':'name'},
2:{'colId':2, 'name':'favorite color'},
3:{'colId':3, 'name':'phone number'}
},
// ...snip... //
I am getting 'currentColumns' via:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: DS.attr('object'),
rows: DS.attr('object'),
currentColumns: function() {
var newColumns = $.map(this.get('columns'), function(k, v) {
return [k];
});
return newColumns;
}.property('columns'),
// ..snip.. //
Here you can see my problem... it's obvious that my 'column' isn't going to have any methods from my controller. I tried something like this:
FakeDB.Adapter = DS.FixtureAdapter.extend();
FakeDB.Adapter.map('FakeDB.Table', {
columns: {embedded: 'load'},
rows: {embedded: 'load'}
});
FakeDB.Columns = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
FakeDB.Rows = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
But I couldn't get {{#each column in columns}} to work with that.
Any suggestions? I'm going to read the docs again and will post back if I find a solution.
Thanks!
EDIT:
So I think I found another solution, but I'm still running into a little issue.
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
});
Still not sure what to replace 'item.tableID == 1' with so that I get items with the tableID referencing to the current page...
Columns are structured as...
FakeDB.Columns.FIXTURES = [
{
id: 1,
tableID: 1,
name: 'name'
},
// ...snip... //
But now I get:
assertion failed: Your application does not have a 'Store' property defined. Attempts to call 'find' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'
I am in fact defining a 'Store' property...
I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Most databases do contain a list of tables, rows and columns. Most web applications contain a fixed set of tables with pre-defined columns and a dynamic list of rows. If this is your first ember app i would recommend starting with something that keeps you on the happy path.
I am in fact defining a 'Store' property...
True but ember is complaining because store is not available before ember app is initialized. Anything that accesses the store should be in a framework hook of some kind. It can't be used when defining your objects, which wouldn't make a lot of sense anyway.
Probably what you meant to do was make a computed property called columns like this:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: function() {
FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
}.property('')
});