FractalLabs
FractalLabs

Reputation: 75

Merging multiple objects in an array with Angular JS based on id

I've searched SO for a while trying to find an answer to this question as I imagine someone's run into this scenario. Please point me in the right direction if an answer is already out there. I have an array of objects containing books. These books are stored in a database and can have one or many writers. For each writer in the writers table, there is a column for book_id(FK), writer_name, and review_id(FK). Therefore if there are multiple writers on a book, there is a row for each writer containing the same information except the name. Here is an example of a JSON response from the DB:

var data = [
{"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
{"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
{"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis", "review_id" : 5},
{"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
{"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
{"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
{"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
{"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];

I would like to filter this array so that I end up with 3 objects, one for each book, that has no repeat values and each writer concatenated as a comma delimited string. Like so:

var data = [
{"id": 1, "writer" : "John Joseph Doe, Daniel Smith, Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis, Jill Steinberg, Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams, Mary Joe Williams, Helen Andrews, Samantha Jones", "review_id" : 3}
];

So I made an attempt to create a function that uses a for loop and a 'first' variable to keep track of which object I'm at based on it's id. My logic is to add each writer to the first instance of each object with a unique id, then remove all the proceeding objects with the same id after the writer has been added. Therefore I'm left with a single object for each book id and a concatenated string of writers separated by commas. The results are close to what I'm looking for, but I can't get it right. I'm not sure if removing the objects from the array is my issue or if I'm taking the entirely wrong approach in general. Here's a working fiddle: http://jsfiddle.net/mlabita37/Lvc0u55v/8987/ . There is a textarea above the table that I'm using as a 'console' to show the final data array, just expand to see the full result.

Here's some snippets of that code:

HTML:

<div ng-controller="BookCtrl">
<table class="table table-striped">
 <thead>
  <tr>
    <th>Book ID</th>
    <th>Writer</th>
    <th>Review ID</th>
  </tr>
</thead>
<tbody>
  <tr ng-repeat="book in books">
    <td>{{ book.id }}</td>
    <td>{{ book.writer }}</td>
    <td>{{ book.review_id }}</td>
    <textarea>{{ books }}</textarea>
  </tr>
</tbody>

Controller:

var data = [
{"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
{"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
{"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis", "review_id" : 5},
{"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
{"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
{"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
{"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
{"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];
combine = function(data){
  var first = data[0];
  for(var i = 1; i < data.length; i++){
    if(data[i].id === first.id){
      first.writer = first.writer + ", " + data[i].writer;
      data.splice(i, 1);
    }
    if(data[i].id !== first.id){
      first = data[i];
      var j = i+1;
      if(data[j].id === first.id){
        first.writer = first.writer + ", " + data[j].writer;
        data.splice(j, 1);
      }
   }
 }  
};
combine(data);
$scope.books = data;

Another idea I had was to use a filter, however I don't have much experience with them and don't know if that would be a better approach. I also wasn't sure if this was something that could/should be handled in my MySQL query instead, or in the backend PHP. I'm under the impression that this is best handled on the front end.

Upvotes: 1

Views: 1113

Answers (2)

Kostas
Kostas

Reputation: 1903

var data = [
{"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
{"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
{"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
{"id": 2, "writer" : "Erin Davis", "review_id" : 5},
{"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
{"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
{"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
{"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
{"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
{"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];

function existsAt(array, key, value) {
    for (var i = 0; i < array.length; i++) {
        if (array[i][key] == value) {
            return i;
        }
    }
    return false;
}

var _data = [];
_data.push(data[0]);

for (var i = 1; i < data.length; i++) {
    var alreadyExistsAt = existsAt(_data, 'id', data[i].id);
    if (alreadyExistsAt !== false) {
        _data[alreadyExistsAt].writer += ', ' + data[i].writer;
    } else {
        _data.push(data[i]);
    }
}

document.body.innerHTML = JSON.stringify(_data);

Upvotes: 1

Andrew Willems
Andrew Willems

Reputation: 12458

I hope the variable names in the code make it fairly self-explanatary.

  • The outer forEach loop selects each review (i.e. currReview) in turn from the complete list of reviews.
  • The inner forEach loop loops over each pre-existing concatenated book review object (i.e. prevReviewsForThisBook...empty at first). This inner loop determines whether the current review is for a book that already has at least one review for it:
    • If it is, then the writer property of that pre-existing review object is updated to include the new writer.
    • If it is not, then the entire current review object is pushed onto the array of pre-existing book review objects.

Note that your id which is presumably your book_id is always identical to your review_id. Is that really what you want? It seems like one of them is either redundant/unnecessary, or one of them is calculated/determined incorrectly. The latter seems possible. For example, are the review_id of the review done by John Joseph Doe and the review_id of the review done by Daniel Smith both supposed to have the same value, 4? I would think that those are separate reviews and thus should have different review_id values, even if they both have the same id/book_id value (because those two people both reviewed the same book). This issue is not central to answering this question, but you might want to re-think that.

var data = allReviews = [
  {"id": 1, "writer" : "John Joseph Doe", "review_id" : 4},
  {"id": 1, "writer" : "Daniel Smith", "review_id" : 4},
  {"id": 1, "writer" : "Thomas Edward Jones", "review_id" : 4},
  {"id": 2, "writer" : "Erin Davis", "review_id" : 5},
  {"id": 2, "writer" : "Jill Steinberg", "review_id" : 5},
  {"id": 2, "writer" : "Laurie Beth Jennings", "review_id" : 5},
  {"id": 3, "writer" : "Emma Jean Williams", "review_id" : 3},
  {"id": 3, "writer" : "Mary Joe Williams", "review_id" : 3},
  {"id": 3, "writer" : "Helen Andrews", "review_id" : 3},
  {"id": 3, "writer" : "Samantha Jones", "review_id" : 3}
];
var reviewsByBook = [];
allReviews.forEach(function(currReview) {
  var currReviewIsForNewBook = true;
  reviewsByBook.forEach(function(prevReviewsForThisBook) {
    if (currReview.id === prevReviewsForThisBook.id) {
      prevReviewsForThisBook.writer += ", " + currReview.writer;
      currReviewIsForNewBook = false;
    }
  });
  if (currReviewIsForNewBook) {
    reviewsByBook.push(currReview);
 }
});
console.log(reviewsByBook);

Upvotes: 1

Related Questions