Search code examples
javascriptangularcheckboxfiltercategories

Angular 2 Multiple Category Checkbox Filter With Reactive Form


I'm trying to do a more complex (for me) filter with 2 different categories (hopefully more), frameBrand & frameColor. I want to filter down to a specific brand, then use those results to filter colors until I have a more specific/narrow search. I'm struggling with filtering 2 categories

With my example JSON below, I'd like to click a brand checkbox to get "anne klein" frames then click a color checkbox "brown" to view the "anne klein - brown" frame. 2 checkboxes each from different "categories". Ideally, I'd be able to expand this to use other categories, like checkboxes for price. I've used StackOverflow to get where I'm at with .filter but haven't seen much with multiple categories and checkboxes using Angular.

{
  "frames": [
    {
      "frameBrand": "altair",
      "frameColor": "Black",
      "frameId": 1,
      "frameName": "A5052",
      "releaseDate": "05-01-2021",
      "framePrice": 25,
      "frameDescription": "this is an altair frame.",
      "imageUrl": "/assets/images/frames/ALO/A5052/A5052_001_LG_01.jpg",
      "categories": [1]
    },
    {
      "frameBrand": "anne klein",
      "frameColor": "Ruby",
      "frameId": 2,
      "frameName": "AK5091",
      "releaseDate": "05-01-2021",
      "framePrice": 45,
      "frameDescription": "this is an anne klein frame.",
      "imageUrl": "/assets/images/frames/AKO/AK5091/AK5091_610_LG_01.jpg",
      "categories": [2]
    },
    {
      "frameBrand": "anne klein",
      "frameColor": "Brown",
      "frameId": 3,
      "frameName": "AK5090",
      "releaseDate": "05-01-2021",
      "framePrice": 45,
      "frameDescription": "this is an anne klein frame.",
      "imageUrl": "/assets/images/frames/AKO/AK5090/AK5090_200_LG_01.jpg",
      "categories": [2]
    },
    {
      "frameBrand": "bebe",
      "frameColor": "Silver",
      "frameId": 4,
      "frameName": "BB5192",
      "releaseDate": "05-01-2021",
      "framePrice": 40,
      "frameDescription": "this is an bebe frame.",
      "imageUrl": "/assets/images/frames/BBO/BB5192/BB5192_001_LG_01.jpg",
      "categories": [3]
    }
  ]
}

In my TS file I have 3 functions, filterFramesByBrand, filterFramesByColor and filterResults. I take the checked checkboxes from brands and colors and push them to different array's in my formGroup when (change)="filterFramesByBrand($event)" occurs. Then I run the filterResults function where I'm trying to make the magic happen. I've gotten the brands to filter as expected but mixing the 2 categories (brands and colors) has me stumped. I've commented out the color section of my filterResults function because it's not working like the brands section for some reason.

checkboxForm: FormGroup;

constructor(private fb: FormBuilder) {}

ngOnInit() {
  this.checkboxForm = this.fb.group({
    brandCheckArray: this.fb.array([]),
    colorCheckArray: this.fb.array([])
  });
}


filteredFrames: IFrames[] = [...this.frames];

  filterFramesByBrand(category) {
    const brandCheckArray: FormArray = this.checkboxForm.get(
      "brandCheckArray"
    ) as FormArray;

    if (category.target.checked) {
      brandCheckArray.push(new FormControl(category.target.value));
    } else {
      let i: number = 0;
      brandCheckArray.controls.forEach((item: FormControl) => {
        if (item.value == category.target.value) {
          brandCheckArray.removeAt(i);
          return;
        }
        i++;
      });
    }
    this.filterResults();
  }

  filterFramesByColor(category) {
    const colorCheckArray: FormArray = this.checkboxForm.get(
      "colorCheckArray"
    ) as FormArray;

    if (category.target.checked) {
      colorCheckArray.push(new FormControl(category.target.value));
    } else {
      let i: number = 0;
      colorCheckArray.controls.forEach((item: FormControl) => {
        if (item.value == category.target.value) {
          colorCheckArray.removeAt(i);
          return;
        }
        i++;
      });
    }
    this.filterResults();
  }

  filterResults() {
    this.filteredFrames = this.frames.filter(el => {
      let frameBrandEl = el.frameBrand
        .split(" ")
        .join("")
        .toLowerCase();
      // let frameColorsEl = el.frameColor
      //   .split(" ")
      //   .join("")
      //   .toLowerCase();

      let brandsChecked = this.checkboxForm.get("brandCheckArray").value;
      // let colorsChecked = this.checkboxForm.get("colorCheckArray").value;

      var fixCheckedBrands = brandsChecked.map(v =>
        v
          .split(" ")
          .join("")
          .toLowerCase()
      );
      // var fixCheckedColors = colorsChecked.map(v =>
      //   v
      //     .split(" ")
      //     .join("")
      //     .toLowerCase()
      // );

      console.log(frameBrandEl);
      // console.log(frameColorsEl);

      console.log(fixCheckedBrands);
      // console.log(fixCheckedColors);

      return frameBrandEl.includes(fixCheckedBrands);
      // return frameColorsEl.includes(fixCheckedColors);
    });

    // if there are no checked checkboxes - show all
    if (this.checkboxForm.value.brandCheckArray.length === 0) {
      this.filteredFrames = this.frames;
    }
  }

Finally: here is a link to my StackBlitz - https://stackblitz.com/edit/angular-ivy-product-filters?file=src%2Fapp%2Fproduct-filter.ts I think with a bit of guidance on the .filter function I could figure this out. Thanks for any help or suggestions.


Solution

  • I'd simplify you logic. Instead of doing this with FormGroup of FormArrays (unless that is not requirement) the best here would be to create FilterComponent which accepts as input exemplary object type CheckboxFilter = { name: string, isChecked: boolean }. Probably you could add id if neccessary. Having that created add Output to your FilterComponent. Now you add to handlers for that BrandChanged and ColorChanged. Based on that you are able to store selected filters and apply them on your frames.

    That even allows you to make it more reacive way. You could decalre BehaviourSubject<string[]> for both Brand and Color and once there is event you could manipulate subject. Later just combainLatest of your frames that came from JSON file or API and apply do it like this:

    combineLatest([frames$, brands$, colors$]).pipe(filter([frames, brands, colors]) => ...) 
    

    Here is working stackblitz