Adding product categories

Product categories allow us to filter the products based on certain criteria, such as books, clothing, and so on. Hierarchies are another important feature to be added so that we can group the categories into sub-categories. For example, within the book category, we can have multiple sub-categories like non-fiction, novels, self-help, and so on.

Adding the sidebar

We are now going to add a new sidebar to the marketplace that shows all the products. This sidebar will list all the products categories in the product-list page:

/* client/app/products/templates/product-list.html *excerpt */

<navbar></navbar>

<div class="container">
  <div class="row">
    <div class="col-md-3" ng-include="'components/sidebar/sidebar.html'"></div>
    <div class="col-sm-6 col-md-9" ng-show="products.length < 1"> No products to show.</div>
    <div class="col-sm-6 col-md-9" ng-repeat="product in products">
      <div class="thumbnail">
        <h3>{{product.title}}</h3>
        <img src="{{product.imageUrl}}" alt="{{product.title}}">
        <figcaption class="figure-caption"> {{product.description | limitTo: 100}} ...</figcaption>
        <div class="caption">
          <p>{{product.price | currency }}</p>
          <p>
            <ngcart-addtocart id="{{product._id}}" quantity="1" quantity-max="9" name="{{product.title}}" price="{{product.price}}" template-url="components/ ngcart/addtocart.html" data="product">Add to Cart</ngcart-addtocart>

            <a ui-sref="viewProduct({id: product._id})" class="btn btn-default" role="button">Details</a>
          </p>
        </div>
      </div>
    </div>
  </div>
</div>

<footer></footer>

We haven't defined it as yet, but the sidebar will contain the products listed according to their categories. Let's create the new sidebar components:

mkdir client/components/sidebar
touch client/components/sidebar/sidebar.{html,scss,controller.js,service.js}

Here is the content for the sidebar.html:

/* client/components/sidebar/sidebar.html */

<div ng-controller="SidebarCtrl" class="sidebar">
  <ul class="nav nav-sidebar">
    <li ng-repeat="category in catalog" ng-class="{active: isActive(category.slug)}">
      <a ui-sref="productCatalog ({slug: category.slug})">{{category.name}}</a>
    </li>
  </ul>
</div>

We will need some CSS styling for the sidebar:

/* client/components/sidebar/sidebar.scss */

/* Hide for mobile, show later */
.sidebar {
  display: none;
}

@media (min-width: 768px) {
  .sidebar {
    display: block;
    padding: 20px;
    overflow-x: hidden;
    overflow-y: auto;
  }
}

/* Sidebar navigation */
.nav-sidebar {
  margin-right: -21px; /* 20px padding + 1px border */
  margin-bottom: 20px;
  margin-left: -20px;
}
.nav-sidebar > li > a {
  padding-right: 20px;
  padding-left: 20px;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
  color: #fff;
  background-color: #428bca;
}

The SidebarCtrl controller is going to pull out all the product categories from products:

/* client/components/sidebar/sidebar.controller.js */

angular.module('meanshopApp')
  .controller('SidebarCtrl', function ($scope, Catalog, $location) {
    $scope.catalog = Catalog.query();

    $scope.isActive = function(route) {
      return $location.path().indexOf(route) > -1;
    };
  });

And finally, we need a service that will retrieve the categories from the database. We do that as follows:

/* client/components/sidebar/sidebar.service.js */

angular.module('meanshopApp')
  .factory('Catalog', function ($resource) {
    return $resource('/api/catalogs/:id');
  });

Now, it's time to move to the backend and create the /api/catalogs route. In the next section, we are going to set up the backend to add categories to the products. We will also create 'slugs'—human friendly URLs—that will be linked to the categories.

Tip

URL slugs

Slugs are human and SEO-friendly URLs. Instead of having a page with a URL identified by an ID such as /categories/561bcb1cf387488206202ab1, it is better to have a URL with a unique and meaningful name, such as /categories/books.

Improving product models and controllers

Let's move to the server side. We will now provide the routes and URLs for filtering the products by categories and will allow us to search for products. For the search, we are going to add MongoDB's full-text indexes, and for the categories, we are going to create a new model:

/* server/api/product/product.model.js *excerpt */

var ProductSchema = new Schema({
  title: { type: String, required: true, trim: true },
  price: { type: Number, required: true, min: 0 },
  stock: { type: Number, default: 1 },
  description: String,
  imageBin: { data: Buffer, contentType: String },
  imageUrl: String,
  categories: [{ type: Schema.Types.ObjectId, ref: 'Catalog', index: true }]
}).index({
  'title': 'text',
  'description': 'text'
});

We haven't created the Catalog model, but we will soon. Notice that we added two text indexes on title and description. That will allow us to search on those fields.

Catalog controller and routes

In order to provide filtering by category and searching, we need to create new routes as follows:

/* server/api/product/index.js *excerpt */

router.get('/', controller.index);
router.get('/:id', controller.show);
router.get('/:slug/catalog', controller.catalog);
router.get('/:term/search', controller.search);
router.post('/', controller.create);
router.put('/:id', controller.update);
router.patch('/:id', controller.update);
router.delete('/:id', controller.destroy);

Now that we are referencing the catalog and search actions in the controller, we need to create them:

/* server/api/product/product.controller.js *excerpt */

var Catalog = require('../catalog/catalog.model');

exports.catalog = function(req, res) {
  Catalog
    .findOne({ slug: req.params.slug })
    .then(function (catalog) {
      var catalog_ids = [catalog._id].concat(catalog.children);
      console.log(catalog_ids, catalog);
      return Product
        .find({'categories': { $in: catalog_ids } })
        .populate('categories')
        .exec();
    })
    .then(function (products) {
      res.json(200, products);
    })
    .then(null, function (err) {
      handleError(res, err);
    });
};

exports.search = function(req, res) {
  Product
    .find({ $text: { $search: req.params.term }})
    .populate('categories')
    .exec(function (err, products) {
      if(err) { return handleError(res, err); }
      return res.json(200, products);
    });
};

For the catalog action, we are performing the following two steps:

  1. Finding the category ID by the slug
  2. Finding all the products that match the category's ID and the IDs of the category's children.

For the search action, we are using MongoDB's $text $search; this is going to work on all the fields which have text indexes, such as title and description. Now, let's create the catalog model.

The catalog model

In our product catalog, we would like to modify the URL based on the category we are showing. So, for instance, to show all the products under the book category, we would like to show a URL like /products/books. For that, we will use a slug.

Let's create the Product catalog and library to help us with the slug:

npm install [email protected] --save

yo angular-fullstack:endpoint catalog

? What will the url of your endpoint be? /api/catalogs
   create server/api/catalog/catalog.controller.js
   create server/api/catalog/catalog.events.js
   create server/api/catalog/catalog.integration.js
   create server/api/catalog/catalog.model.js
   create server/api/catalog/catalog.socket.js
   create server/api/catalog/index.js
   create server/api/catalog/index.spec.js

Now let's modify the catalog model as follows:

/* server/api/catalog/catalog.model.js */

var mongoose = require('bluebird').promisifyAll(require('mongoose'));
var Schema = mongoose.Schema;
var slugs = require('mongoose-url-slugs');

var CatalogSchema = new Schema({
  name: { type: String, required: true},
  parent: { type: Schema.Types.ObjectId, ref: 'Catalog' },
  ancestors: [{ type: Schema.Types.ObjectId, ref: 'Catalog' }],
  children: [{ type: Schema.Types.ObjectId, ref: 'Catalog' }]
});

CatalogSchema.methods = {
  addChild: function (child) {
    var that = this;
    child.parent = this._id;
    child.ancestors = this.ancestors.concat([this._id]);
    return this.model('Catalog').create(child).addCallback (function (child) {
      that.children.push(child._id);
      that.save();
    });
  }
}

CatalogSchema.plugin(slugs('name'));

module.exports = mongoose.model('Catalog', CatalogSchema);

With this catalog model, we can not only add nested categories, but also keep track of the categories' ancestors and children. Also, notice that we are adding a plugin to generate the slugs based on the name.

One way to test that everything is working as intended is through the unit tests; for more information, refer to https://raw.githubusercontent.com/amejiarosario/meanshop/ch8/server/api/catalog/catalog.model.spec.js.

From this unit, we can see that we can find products based on catalog._id; we can also find multiple ones using $in.

Seeding products and categories

In order to have a predefined list of products and categories, it will be a good idea to seed the development database with it. Replace the previous product's seed with the following:

/* server/config/seed.js *excerpt */

var Catalog = require('../api/catalog/catalog.model');
var mainCatalog, home, books, clothing;

Catalog
  .find({})
  .remove()
  .then(function () {
    return Catalog.create({ name: 'All'});
  })
  .then(function (catalog) {
    mainCatalog = catalog;
    return mainCatalog.addChild({name: 'Home'});
  })
  .then(function (category) {
    home = category._id;
    return mainCatalog.addChild({name: 'Books'});
  })
  .then(function (category) {
    books = category._id;
    return mainCatalog.addChild({name: 'Clothing'});
  })
  .then(function (category) {
    clothing = category._id;
    return Product.find({}).remove({});
  })
  .then(function() {
    return Product.create({
      title: 'MEAN eCommerce Book',
      imageUrl: '/assets/uploads/meanbook.jpg',
      price: 25,
      stock: 250,
      categories: [books],
      description: 'Build a powerful e-commerce application ...'
    }, {
      title: 'tshirt',
      imageUrl: '/assets/uploads/meantshirt.jpg',
      price: 15,
      stock: 100,
      categories: [clothing],
      description: 'tshirt with the MEAN logo'
    }, {
      title: 'coffee mug',
      imageUrl: '/assets/uploads/meanmug.jpg',
      price: 8,
      stock: 50,
      categories: [home],
      description: 'Convert coffee into MEAN code'
    });
  })
  .then(function () {
    console.log('Finished populating Products with categories');
  })
  .then(null, function (err) {
    console.error('Error populating Products & categories: ', err);
  });

We use promises to avoid the so-called callback hell. We create each one of the categories first and save them in variables. Later, we create each of the products and associate them with its corresponding category. If any errors occur in the process, they are catch'ed at the very end.

Now when we run grunt serve, we see that the new categories and products are being created.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.125.51