sosNiLa
sosNiLa

Reputation: 337

How to perform multiple array filtering

I have two arrays which have mutual ID, so I am able to connect them together but I need help with filtering.

So I have a list of first array and check box in front of it. If I select one check box, I am performing filtering and I am displaying another item from second array inside the table (by checking up the mutual ID).

I need help because if I perform multiple selection, I expect to have multiple results, and not just one. This is the image how it looks like and the code I am using for filtering.

So if I select check box for Item 1 and Item 2 I expect to have 3 results (ids 10, 20(x2)) from apps array in the lower table. Of course if I deselect check box, they should be removed from the lower table. Because I am using only .filter method, I am able to get only one result (from last selected checkbox).

  let items = [
    {id:1, name:'Item 1', appId:10},
    {id:2, name:'Item 2', appId:20},
    {id:3, name:'Item 3', appId:20},
    {id:4, name:'Item 4', appId:30}
  ]
  
  let apps = [
    {id:10, address:'Some street 1', city:'City 1'},
    {id:20, address:'Some street 2', city:'City 2'},
    {id:20, address:'Some street 2', city:'City 2'},
    {id:30, address:'Some street 3', city:'City 3'}
  ]

this.dataSource = this.items.filter(x => x.appId == apps.id)

enter image description here

Thanks

UPDATE

I have managed to do it by Rohit's answer, but I have a problem now because when I select check box in first array, all previously selected check boxes in second array are being removed, so the question is how to keep them?

I have added new images, before and after, so after selecting check box Item 1, I expect check boxes for Some street 9 and Some street 10 to remain selected.

My html.

<table mat-table [dataSource]="dataSourceObjects" class="mat-elevation-z8">
  <ng-container matColumnDef="objectChBox">
    <th mat-header-cell *matHeaderCellDef></th>
    <td mat-cell *matCellDef="let element">
      <mat-checkbox
        class=""
        [checked]="element.chbxActive"
        (change)="checkSingleObject($event, element)"
      >
      </mat-checkbox>
    </td>
  </ng-container>

  <ng-container matColumnDef="address">
    <th mat-header-cell *matHeaderCellDef>Address</th>
    <td mat-cell *matCellDef="let element">{{element.address}}</td>
  </ng-container>

  <ng-container matColumnDef="city">
    <th mat-header-cell *matHeaderCellDef>City</th>
    <td mat-cell *matCellDef="let element">{{element.city}}</td>
  </ng-container>

  <tr mat-header-row *matHeaderRowDef="displayedColumnsObjects"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumnsObjects;"></tr>
</table>

before after

Upvotes: 1

Views: 567

Answers (5)

Roh&#236;t J&#237;ndal
Roh&#236;t J&#237;ndal

Reputation: 27202

Observation as per your current implementation, As apps is an array. It's items should be access via index, we can not directly access using dot(.) notation.

As user can select multiple items, we can just check for the id in the apps array based on the appId selected from the items array using checkbox.

Just for a demo purpose, I am directly assigning the appId 10 and 20 in an array and then will fetch the mapped results from apps array based on Array#filter method along with Array#includes method.

// Apps array
let apps = [
  {id:10, address:'Some street 1', city:'City 1'},
  {id:20, address:'Some street 2', city:'City 2'},
  {id:20, address:'Some street 2', city:'City 2'},
  {id:30, address:'Some street 3', city:'City 3'}
];

// Selected items array.
const selctedItems = [10, 20];

const result = apps.filter(({ id }) => selctedItems.includes(id));

// result
console.log(result);

Update : Full working demo as per the author request (Descriptive comments have been added in the below code snippet itself).

// Input Array 1
let items = [
  {id:1, name:'Item 1', appId:10},
  {id:2, name:'Item 2', appId:20},
  {id:3, name:'Item 3', appId:20},
  {id:4, name:'Item 4', appId:30}
];

// Input Array 2
const apps = [
  {id:10, address:'Some street 1', city:'City 1'},
  {id:20, address:'Some street 2', city:'City 2'},
  {id:30, address:'Some street 3', city:'City 3'}
];

// Element on which checkboxes will get bind. 
var myDiv = document.getElementById("checkBoxes");

// Loop through each object of "items" array to create checkbox dynamically and bind in the "myDiv" variable which referes to "checkBoxes" element.
for (var i = 0; i < items.length; i++) {
  var checkBox = document.createElement("input");
  var label = document.createElement("label");
  checkBox.type = "checkbox";
  checkBox.value = items[i].appId;
  myDiv.appendChild(checkBox);
  myDiv.appendChild(label);
  label.appendChild(document.createTextNode(items[i].name));
}

// Getting all the checkboxes elements for iteration.
var cbs = document.querySelectorAll('input[type="checkbox"]');

// This variable will hold the selected checkboxes values.
let selectedItems = [];

// Iterate over each checkbox element and triggering an "onchange" event.
cbs.forEach(function(item) {
  item.onchange = getCheckBoxValue;
});

// Method which is responsible to get all the selected checkbox values into an array.
function getCheckBoxValue() {
  selectedItems = [];
  cbs.forEach(function(item) {
    if (item.checked) {
      if (!selectedItems.includes(item.value)) {
        selectedItems.push(item.value)
      }
    }
  });
  getFilteredValuesFromAnotherArr();
}

// Final method which is basically used to fetch the selected values from a second array based on the first array selections.
function getFilteredValuesFromAnotherArr() {
  console.log(apps.filter(({ id }) => selectedItems.includes(id.toString())));
}
<div id="checkBoxes"></div>

Upvotes: 3

jackwaayer
jackwaayer

Reputation: 119

My advice would be to merge the two arrays. This will remove a lot of complexity and make your code super easy to read. There are many ways to solve this problem, but the following is how I would do it.

Firstly I create a dictionary for the app array using the reduce function. This gives me the benefit of having indexes which correlate with the ids in the items array, allowing me to directly access each app item without looping the entire array.

Once I have the dictionary, I can easily perform the merge by mapping the items array and creating a new object for each item containing the original item + the app item from the appDict.

Now you have a single array with everything you need to solve the problem, and you remain within the O(n) time complexity.

const items = [
  {id:1, name:'Item 1', appId:10},
  {id:2, name:'Item 2', appId:20},
  {id:3, name:'Item 3', appId:20},
  {id:4, name:'Item 4', appId:30}
]
const apps = [
  {id:10, address:'Some street 1', city:'City 1'},
  {id:20, address:'Some street 2', city:'City 2'},
  {id:30, address:'Some street 3', city:'City 3'}
];
const appDict = apps.reduce((acc, {id, ...rest})=> ({...acc, [id]: rest}), {})
const combinedArr = items.map(item=>({...item, ...appDict[item.appId]}))

Upvotes: 1

Mulan
Mulan

Reputation: 135227

The answer using .filter(...includes...).map(...) combines nested linear operations which results in quadratic time complexity. The answer using .filter(...findIndex...) has the same problem.

Here's a minimal reproducible example. This solution uses -

  • efficient Set for fast O(1) lookups
  • useMemo to avoid wasted computation
  • flatMap to combine filter->map in a single pass

function App({ items, apps }) {
  const [selected, setSelected] = React.useState(_ => new Set())
  const selectedApps = React.useMemo(
    () => new Set(Array.from(selected, s => s.appId)),
    [selected]
  )
  const select = item => event => {
    const s = new Set(selected)
    if (s.has(item)) s.delete(item)
    else s.add(item)
    setSelected(s)
  }
  return <div>
    {items.map((item, key) =>
      <div>
        <input
          key={key}
          type="checkbox"
          checked={selected.has(item)}
          onClick={select(item)}
        />
        {item.name}
      </div>
    )}
    {apps.flatMap((app, key) =>
      selectedApps.has(app.id)
        ? [<pre key={key}>{JSON.stringify(app)}</pre>]
        : []
    )}
  </div>
}

const items = [{id:1, name:'Item 1', appId:10},{id:2, name:'Item 2', appId:20},{id:3, name:'Item 3', appId:20},{id:4, name:'Item 4', appId:30}]
const apps = [{id:10, address:'Some street 1', city:'City 1'},{id:20, address:'Some street 2', city:'City 2'},{id:20, address:'Some street 2', city:'City 2'},{id:30, address:'Some street 3', city:'City 3'}]

ReactDOM.render(<App items={items} apps={apps} />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Upvotes: 0

Aqil Contractor
Aqil Contractor

Reputation: 73

[UPDATED]

This code should suit your needs exactly:

let items = [
    {id: 1, name: 'Item 1', appId: 10},
    {id: 2, name: 'Item 2', appId: 20},
    {id: 3, name: 'Item 3', appId: 20},
    {id: 4, name: 'Item 4', appId: 30}
]

let apps = [
    {id: 10, address: 'Some street 1', city: 'City 1'},
    {id: 20, address: 'Some street 2', city: 'City 2'},
    {id: 20, address: 'Some street 2', city: 'City 2'},
    {id: 30, address: 'Some street 3', city: 'City 3'}
]

this.dataSource = items.filter(x => apps.findIndex(y => y.id === x.appId) >= 0);

Explanation

We run filter on the items array, which goes through every element of that array, and checks if the function you pass in returns true or not for that element. It adds elements that pass this test to a new array, and returns that array.

The function we pass in for that check, x => apps.findIndex(y => y.id === x.appId) >= 0 uses another utility function in Arrays, findIndex, which works a lot like filter. The exception is that it only returns one item, and the place it is in the array. We pass in the function y => y.id === x.appId, which sees if the item we're checking in the apps array is the one we're looking for. If it doesn't find the item, it returns -1, so we just check against it.

I hope this helps~!!

Upvotes: 0

Wesley LeMahieu
Wesley LeMahieu

Reputation: 2613

If you want to keep track of multiple items, consider this simple example. Pretend App ID 10 and 30 are selected. You need to map the app array to contain the original information as well as the selected-item results like this:

let items = [
  { id: 1, name: "Item 1", appId: 10 },
  { id: 2, name: "Item 2", appId: 20 },
  { id: 3, name: "Item 3", appId: 20 },
  { id: 4, name: "Item 4", appId: 30 },
];

let apps = [
  { id: 10, address: "Some street 1", city: "City 1" },
  { id: 20, address: "Some street 2", city: "City 2" },
  { id: 20, address: "Some street 2", city: "City 2" },
  { id: 30, address: "Some street 3", city: "City 3" },
];

const selectedAppIDs = [10, 30];

const result = apps
  .filter((app) => selectedAppIDs.includes(app.id))
  .map((app) => ({
    ...app,
    selectedItems: items.filter((x) => x.appId === app.id),
  }));

console.log(JSON.stringify(result));

Results:

[{"id":10,"address":"Some street 1","city":"City 1","selectedItems":[{"id":1,"name":"Item 1","appId":10}]},{"id":30,"address":"Some street 3","city":"City 3","selectedItems":[{"id":4,"name":"Item 4","appId":30}]}]

Check out this example

Upvotes: 0

Related Questions