Angular - category-sidebar

Θα δημιουργήσουμε το category-sidebar component, το οποίο περιέχει δύο φίλτρα για τα προϊόντα. Ο χρήστης θα μπορεί, να αναζητήσει προϊόντα συγκεκριμένων εταιριών ή/και μέσα σε ένα συγκεκριμένο διάστημα τιμών(10-20ευρώ).

Εταιρίες

Για την αρχικοποίηση των τιμών των φίλτρων, ο client θα ζητάει τον αριθμό των προϊόντων της κάθε εταιρίας, σε μια κατηγορία. Αν επιλεχθεί κάποιο φίλτρο από τα διαστήματα τιμών μετά, τότε στέλνει μια παράμετρο range, για να πάρει τον αριθμό των προϊόντων της κάθε εταιρίας, σε ένα διάστημα τιμών.

Διαστήματα τιμών

Για την αρχικοποίηση των τιμών των φίλτρων, ο client θα ζητάει τον αριθμό των προϊόντων ανά διάστημα τιμών. Αν επιλεχθεί κάποιο φίλτρο από τις εταιρίες μετά, τότε στέλνει μια παράμετρο brand, για να πάρει τον αριθμό των προϊόντων στο συγκεκριμένο διάστημα τιμών, που ανήκουν σε αυτές τις εταιρίες.

Spring Boot

Αρχικά θα χρειαστούμε μια μέθοδο, η οποία θα επιστρέφει τις εταιρίες των προϊόντων μιας κατηγορίας.

Προσθέτουμε την ακόλουθη μέθοδο στο ProductRepository. Θα επιστρέφει τη λίστα των εταιριών, πραγματοποιώντας το ερώτημα JPQL "select DISTINCT prod.brand from Product prod where prod.category.id = :id".

                
                        package springeshop.repositories;

                        @Repository
                        public interface ProductRepository  extends JpaRepository<Product, Integer>  {

                          ....
                          @Query("select DISTINCT prod.brand from Product prod where prod.category.id = :id")
                          List<Brand> findCategoryBrands(@Param("id") int id);
                        } 
                
            

Προσθέτουμε την μέθοδο στο ProductService και ProductServiceImpl.

                
                        package springeshop.service;

                        public interface ProductService{

                          ...
                          List<Brand> findCategoryBrands(@Param("id") int id);
                        } 
                
            
                
                            package springeshop.service;

                            @Service("productService")
                            @Transactional
                            public class ProductServiceImpl implements ProductService{

                              ...

                              @Override
                              public List<Brand> findCategoryBrands(@Param("id") int id){
                                return productRepository.findCategoryBrands(id);
                              }
                        }
                
            

Δημιουργούμε έναν BrandApiController στο springeshop.controller

                
                            package springeshop.controller;

                            @RestController
                            @RequestMapping("/api")
                            public class BrandApiController{
                        
                               public static final  Logger logger = LoggerFactory.getLogger(BrandApiController.class);
    
                               @Autowired
                               private  ProductService productService;
    
                               @Autowired
                               private  CategoryService categoryService;

                                ...
                
            

Προσθέτουμε τη μέθοδο getCategoryBrands στον BrandApiController. Εάν δεν υπάρχει κατηγορία με αυτό το όνομα, επιστρέφουμε status 404 NOT FOUND.

                
                           @RequestMapping(value = "/brands", method = RequestMethod.GET)
                           public ResponseEntity<?> getCategoryBrands(@RequestParam((@value = "category") String category{

                           Category requestedCategory = categoryService.findByName(getCorrectCategoryName(category));

                           if(requestedCategory == null){
                             logger.error("Category with name {} not found.", name);
                             return new ResponseEntity<>(new ErrorMessage("Category with name " + name + " not found"), HttpStatus.NOT_FOUND);
                           }
                
            

Εάν η λίστα τον εταιριών είναι κενή, επιστρέφουμε status 204 NO CONTENT, αλλιώς τις ταξινομούμε σε αύξουσα αλφαβητική σειρά και επιστρέφουμε την λίστα με status 200.

                
                           List<Brand> brands = productService.findCategoryBrands(requestedCategory.getId());

                           if(brands.isEmpty())){
                             return new ResponseEntity<>(HttpStatus.NO_CONTENT);
                           }

                           brands.sort((Brand brand1, Brand brand2) -> brand1.getName().compareTo(brand2.getName()));
                           return new ResponseEntity<List<Brand>>(brands, HttpStatus.OK);
                
            

Παράδειγμα αιτήματος με Postman

Brands Api

Θα δημιουργήσουμε τις μεθόδους, οι οποίες θα μας επιστρέφουν τον αριθμό των προϊόντων

Προσθέτουμε τις ακόλουθες μέθοδους στο ProductRepository, οι οποίες θα χρησιμοποιηθούν, όταν αρχικοποιηθούν τα φίλτρα. Τα ερωτήμα είναι πάνω από τις υπογραφές των μεθόδων.

                
                          ....
                          @Query("select Count (prod) from Product prod where prod.category.id = :categoryid and prod.brand.id = :brandid")
                          int findNumberOfProductsOfBrandInCategory(@Param("categoryid") int categoryid, @Param("brandid") int brandid);

                          @Query("select Count (prod) from Product prod where prod.category = :category and prod.price between :min and :max")
                          int findNumberOfProductsWithinPriceRange(@Param("category") Category category, @Param("min") double min, @Param("max") double max);
                
            

Προσθέτουμε τις ακόλουθες μεθόδους στο ProductService.

                
                        ....
                        int findNumberOfProductsOfBrandInCategory(int categoryid, int brandid);
                        int findNumberOfBrandProductsWithinSpecificRangesInCategory(Category category, Brand brand, List<double[]< priceRanges);

                        int findNumberOfProductsWithinPriceRange(Category category, double min, double max);
                        int findNumberOfSpecificBrandsProductsWithinPriceRange(Category category,  double min, double max, List<Brand< brands);
                
            

Προσθέτουμε τις υλοποιήσεις των μεθόδων, όταν δεν είναι επιλεγμένα τα αντίστοιχα φίλτρα

                
                            package springeshop.service;

                            @Service("productService")
                            @Transactional
                            public class ProductServiceImpl implements ProductService{

                              @Autowired
                              private  EntityManager entityManager;

                              ...

                              @Override
                              public int findNumberOfProductsOfBrandInCategory(int categoryid, int brandid) {
                                return productRepository.findNumberOfProductsOfBrandInCategory(categoryid, brandid);
                              }

                              @Override
                              public int findNumberOfProductsWithinPriceRange(Category category, double min, double max) {
                                return productRepository.findNumberOfProductsWithinPriceRange(category, min, max);
                              }
                        }
                
            

Η ακόλουθη μέθοδος findNumberOfSpecificBrandsProductsWithinPriceRange(), επιστρέφει τον αριθμό των προϊόντων ανά διάστημα τιμών, όταν ο χρήστης έχει επιλέξει μία ή περισσότερες εταιρίες. Θα χρησιμοποιήσουμε το Criteria API, επειδή δεν γνωρίζουμε από πριν, πόσες εταιρίες θα είναι επιλεγμένες.

Δημιουργούμε το query root

                
                              @Override
                              public int findNumberOfSpecificBrandsProductsWithinPriceRange(Category category, double min, double max, List<Brand> brands){

                              CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
                              CriteriaQuery<Product> criteriaQuery = criteriaBuilder.createQuery(Product.class);

                              Root<Product> productsRoot = criteriaQuery.from(Product.class);
                
            

Με το παρακάτω for, δημιουργούμε συνθήκες του WHERE, "prod.brand = :brand." και τα προσθέτουμε σε ένα array.

                
                              List<Predicate> brandPredicateList = new ArrayList<>();

                              for(int i=0; i < brands.size(); i++){
                                brandPredicateList.add(criteriaBuilder.equal(productsRoot.get("brand"), brands.get(i)));

                              Predicate[] brandsPredicateArray = new Predicate[brandPredicateList.size()];
                              brandPredicateList.toArray(brandsPredicateArray);
                
            

Δημιουργούμε τις συνθήκες "prod.category = :category" και "product.price between :min and :max"

                
                              Predicate categoryPredicate = criteriaBuilder.equal(productsRoot.get("category"), category);
                              Predicate priceRangePredicate = criteriaBuilder.between(productsRoot.get("price"), min, max);
                              Predicate brandsPredicate = criteriaBuilder.or(brandsPredicateArray);
                
            

Δημιουργούμε το τελικό ερώτημα "select Count (prod) from Product prod where prod.category = :category and prod.price between :min and ( prod.brand = :brand1 or prod.brand2 .....)", το εκτελεί ο entityManager και επιστρέφουμε τον αριθμο των αποτελεσμάτων.

                
                              criteriaQuery.where(criteriaBuilder.and(categoryPredicate, priceRangePredicate, brandsPredicate));
                              int number = 0;
                              number = entityManager.createQuery(criteriaQuery).getResultList().size();
                              return number;
                
            

Η ακόλουθη μέθοδος findNumberOfBrandProductsWithinSpecificRangesInCategory, επιστρέφει τον αριθμό των προϊόντων ανά εταιρία, όταν ο χρήστης έχει επιλέξει ένα ή περισσότερα διαστήματα τιμών.

Δημιουργούμε το query root

                
                              @Override
                              public findNumberOfBrandProductsWithinSpecificRangesInCategory(Category category, Brand brand, List<double[]> priceRanges) {

                              CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
                              CriteriaQuery<Product> criteriaQuery = criteriaBuilder.createQuery(Product.class);

                              Root<Product> productsRoot = criteriaQuery.from(Product.class);
                
            

Με το παρακάτω for, δημιουργούμε συνθήκες του WHERE, "prod.price between :range1 and :range2" και τα προσθέτουμε σε ένα array.

                
                              List<Predicate> priceRangePredicateList = new ArrayList<>();

                              for(double[] range : priceRanges){
                                priceRangePredicateList.add(criteriaBuilder.between(productsRoot.get("price"), range[0], range[1]));
                              }

                              Predicate[] priceRangePredicateArray = new Predicate[priceRangePredicateList.size()];
                              priceRangePredicateList.toArray(priceRangePredicateArray);
                
            

Δημιουργούμε τις συνθήκες "prod.category = :category" και "prod.brand = :brand"

                
                              Predicate categoryPredicate = criteriaBuilder.equal(productsRoot.get("category"), category);
                              Predicate brandsPredicate = criteriaBuilder.equal(productsRoot.get("brand"), brand);
                              Predicate priceRangePredicate = criteriaBuilder.or(priceRangePredicateArray);
                
            

Δημιουργούμε το τελικό ερώτημα "select Count (prod) from Product prod where prod.category = :category and prod.brand = :brand ( prod.price between :range1 and :range 2 or prod.price between :range3 and :range 4.....)", το εκτελεί ο entityManager και επιστρέφουμε τον αριθμο των αποτελεσμάτων.

                
                              criteriaQuery.where(criteriaBuilder.and(categoryPredicate, brandsPredicate, priceRangePredicate));
                              int number = 0;
                              number = entityManager.createQuery(criteriaQuery).getResultList().size();
                              return number;
                
            

Δημιουργούμε το τελικό ερώτημα "select Count (prod) from Product prod where prod.category = :category and prod.brand = :brand ( prod.price between :range1 and :range 2 or prod.price between :range3 and :range 4.....)", το εκτελεί ο entityManager και επιστρέφουμε τον αριθμο των αποτελεσμάτων.

                
                              criteriaQuery.where(criteriaBuilder.and(categoryPredicate, brandsPredicate, priceRangePredicate));
                              int number = 0;
                              number = entityManager.createQuery(criteriaQuery).getResultList().size();
                              return number;
                
            
CategoryApiController

Θα προσθέσουμε τις κατάλληλες μεθόδους στον CategoryApiController. Πρώτα θα δημιουργήσουμε μια μέθοδο στο BrandService, η οποία θα παίρνει τις παραμέτρους των εταιριών και θα επιστρέφει μια λίστα από Brand entities.

                
                        package springeshop.service;

                        public interface BrandService{

                          ...
                          List<Brand> findSpecificBrands(String[] brands);
                        } 
                
            
                
                            package springeshop.service;

                            @Service("brandService")
                            @Transactional
                            public class BrandServiceImpl implements BrandService{

                              @Autowired
                              private EntityManager entityManager;

                              ...

                              @Override
                              public List<Brand> findSpecificBrands(String[] brands) {
                        
                              CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
                              CriteriaQuery<Brand> criteriaQuery = criteriaBuilder.createQuery(Brand.class);
                              Root<Brand> productsRoot = criteriaQuery.from(Brand.class);  
                        
                              Predicate[] predicates = new Predicate[brands.length];

                              for(int i=0; i < predicates.length; i++){
                                predicates[i] = criteriaBuilder.equal(requestedBrands.get("name"), brands[i]);
                              }

                              criteriaQuery.where(criteriaBuilder.or(predicates));
                              return entityManager.createQuery(criteriaQuery).getResultList();

                              }
                        }
                
            

Προσθέτουμε κάποιες σταθερές στο Constants.java

                
                            package springeshop.util;

                            public final class Constants {

                              ....

                              private static final double[]  PRICE_RANGE_ZERO_TO_TEN = { 0.0 ,9.99};
                              private static final double[]  PRICE_RANGE_TEN_TO_TWENTY = { 10.0 ,19.99};
                              private static final double[]  PRICE_RANGE_TWENTY_TO_THIRTY = { 20.0 ,29.99};
                              private static final double[]  PRICE_RANGE_THIRTY_TO_FIFTY = { 30.0 ,50.0};
                        }
                
            

Δημιουργούμε τις κλάσεις-μοντέλα ProductsPerPriceRange και ProductsPerBrand

                
                        public class ProductsPerPriceRange{

                           private double min;
                           private double max;
                           private int number;
                           private int rangeId;

                           public ProductsPerPriceRange() {}

                           //Getters - Setters ...

                        } 
                
            
                
                        public class ProductsPerBrand{


                           private String brand;
                           private int number;

                           public ProductsPerBrand() {}

                           //Getters - Setters ...

                        } 
                
            

Δημιουργούμε δύο βοηθητικές μεθόδους στον CategoryApiController, οι οποίες θα επιστρέφουν το minx και το max του κάθε εύρους τιμών.

                
                        public double getRangeMin(String range){

                          double min = 0.0;

                          &if(range.equals("1")){
                            min = Constants.PRICE_RANGE_ZERO_TO_TEN[0];
                          }else if(range.equals("2")){
                            min = Constants.PRICE_RANGE_TEN_TO_TWENTY[0];
                          else if(range.equals("3")){
                            min = Constants.PRICE_RANGE_TWENTY_TO_THIRTY[0];
                          }else if(range.equals("4")){
                            min = Constants.PRICE_RANGE_THIRTY_TO_FIFTY[0];
                          }

                          return min;
                        }
                
            
                
                        public double getRangeMax(String range){

                          double max = 0.0;

                          &if(range.equals("1")){
                            max = Constants.PRICE_RANGE_ZERO_TO_TEN[1];
                          }else if(range.equals("2")){
                            max = Constants.PRICE_RANGE_TEN_TO_TWENTY[1];
                          else if(range.equals("3")){
                            max = Constants.PRICE_RANGE_TWENTY_TO_THIRTY[1];
                          }else if(range.equals("4")){
                            max = Constants.PRICE_RANGE_THIRTY_TO_FIFTY[1];
                          }

                          return max;
                        }
                
            

Προσθέτουμε τη μέθοδο getCategoryRangeProductsNumber στον controller. Αν δεν υπάρχει η ζητούμενη κατηγορία, επιστρέφουμε status 404 NOT FOUND.

                
                           @RequestMapping(value = "/categories/{name}/ranges/{rangeid}/products/count", method = RequestMethod.GET)
                           public ResponseEntity<?> getCategoryRangeProductsNumber(@PathVariable((@value = "name") String name,
                                  @PathVariable((@value = "rangeid") String rangeid,
                                  @RequestParam((@value = "brand", required = false) String[] brands{

                           Category category = categoryService.findByName(getCorrectCategoryName(name));

                           if(requestedCategory == null){
                             logger.error("Category with name {} not found.", name);
                             return new ResponseEntity<>(new ErrorMessage("Category with name " + name + " not found"), HttpStatus.NOT_FOUND);
                           }
                
            

Εάν δεν είναι κάποιο φίλτρο επιλεγμένο καλούμε την findNumberOfProductsWithinPriceRange, αλλιώς βρίσκουμε τα αντίστοιχα Brand entities και το παιρνάμε στην findNumberOfSpecificBrandsProductsWithinPriceRange.

                
                           ProductsPerPriceRange ppNumber = new ProductsPerPriceRange();

                           if(brands == null){
                             ppNumber.setNumber(productService.findNumberOfProductsWithinPriceRange(category, getRangeMin(rangeid), getRangeMax(rangeid)));
                           }else{
                             List<Brand> brandPredicateList = new ArrayList<>();
                              brandList = brandService.findSpecificBrands(brands);
                             ppNumber.setNumber(productService.findNumberOfSpecificBrandsProductsWithinPriceRange(category, getRangeMin(rangeid), getRangeMax(rangeid), brandList));
                           }
                
            

Προσθέτουμε min, max του διαστήματος και το id του και το επιστρέφουμε με status 200 OK.

                
                          ppNumber.setMin(getRangeMin(rangeid));
                          ppNumber.setMax(getRangeMax(rangeid));
                          ppNumber.setRangeId(Integer.parseInt(rangeid));
                          return new ResponseEntity<ProductsPerPriceRang<(ppNumber, HttpStatus.OK);
                
            

Παράδειγμα αιτήματος με Postman

Filter Ranges Api

Προσθέτουμε τη μέθοδο getCategoryBrandProductsNumber στον controller. Αν δεν υπάρχει η κατηγορία ή η εταιρία, επιστρέφουμε status 404 NOT FOUND.

                
                           @RequestMapping(value = "/categories/{categoryname}/brands/{brandname}/products/count", method = RequestMethod.GET)
                           public ResponseEntity<?> getCategoryBrandProductsNumber(@PathVariable((@value = "categoryname") String categoryname,
                                  @PathVariable((@value = "brandname") String brandname,
                                  @RequestParam((@value = "range", required = false) String[] ranges{

                           Category category = categoryService.findByName(getCorrectCategoryName(categoryname));

                           if(category == null){
                             logger.error("Category with name {} not found.", categoryname);
                             return new ResponseEntity<>(new ErrorMessage("Category with name " + categoryname + " not found"), HttpStatus.NOT_FOUND);
                           }

                           if(!brandService.doesBrandExist(brandname)){
                             logger.error("Brand with name {} does not  exist.", brandname);
                             return new ResponseEntity<>(new ErrorMessage("Brand with name {} does not  exist" not found"), HttpStatus.NOT_FOUND);
                           }
                
            

Εάν δεν είναι κάποιο διάστημα τιμών καλούμε την findNumberOfProductsOfBrandInCategory, αλλιώς δημιουργούμε μια λίστα από διαστήματα τιμών, από την παράμετρο range και το παιρνάμε στην findNumberOfBrandProductsWithinSpecificRangesInCategory.

                
                           int productsNumber = 0;

                           if(ranges == null){
                             productsNumber = productService.findNumberOfProductsOfBrandInCategory(category.getId(), brandService.findByName(brandname).getId());
                           }else{
                             List<double[]> priceRangeList = new ArrayList<>();
                             for(String range : ranges){
                               double[] rangeValues = new double[2];
                               rangeValues[0] = getRangeMin(range);
                               rangeValues[1] = getRangeMax(range);
                               priceRangeList.add(rangeValues);
                             }

                             productsNumber = productService.findNumberOfBrandProductsWithinSpecificRangesInCategory(category, brandService.findByName(brandname), priceRangeList);
                           }
                
            

Προσθέτουμε τον αριθμό και το όνομα της εταιρίας και το επιστρέφουμε με status 200 OK.

                
                          ProductsPerBrand pNumber =  new ProductsPerBrand();
                          pNumber.setNumber(productsNumber);
                          pNumber.setBrand(brandname);
                          return new ResponseEntity<ProductsPerBrand<(pNumber, HttpStatus.OK);
                
            

Παράδειγμα αιτήματος με Postman

Filter Ranges Api
category-sidebar component

Δημιουργούμε το component category-sidebar και το τοποθετούμε στο φάκελο components.

                    
                                ng generate component category-sidebar
                    
                

category.service.ts

Category Service 2

category-sidebar.component.html

Το html αρχείο περιέχει δύο λίστες με checkboxes.

Category Sidebar Html

category-sidebar.component.css

Category Sidebar Css

Δημιουργούμε τα αντίστοιχα μοντέλα ProductsPerBrand και ProductsPerPriceRange.

Products Brands Products Price Range

Κάθε φορά που αλλάζει ο χρήστης κατηγορία, καλείται η μέθοδος ngOnChanges(), η οποία καλεί την updateSidebar(), για να πραγματοποίησει τα HTTP αιτήματα και να αρχικοποιήσει τα φίλτρα

category-sidebar.component.ts

Category sidebar Ts

Όταν ο χρήστης κάνει κλικ σε κάποια εταιρία (event), εκτελείται η μέθοδος onSelectedBrand(), η οποία ενημερώνει τον αριθμό των προϊόντων ανα διάστημα τιμών. Αντίστοιχα, όταν κάνει κλικ σε κάποιο διάστημα (event), εκτελείται η μέθοδος onSelectedPriceRange(), η οποία ενημερώνει τον αριθμό των προϊόντων ανα εταιρία.

Category sidebar Ts
Τελική εμφάνιση σελίδας
Category Page Image