Building our Add Products page

Now that we have Facebook authentication and the AWS SDK set up, we’ll start working on the page to allow users to upload their products for sale.

We start by building our method that will insert the data into the DynamoDB table.

Saving data in DynamoDB tables

Within our AWSservice provider, we will create our new function to save the product data as follows:

saveProductData: function(newProduct) {

    var timestamp = new Date().getTime();
    var UUID = newProduct.userId + "-" + timestamp;
    var productData = {
        Item: {
            'product_id': {S: UUID },
            'category': { S: newProduct.category },
            'title': { S: newProduct.title},
            'description': {S: newProduct.description},
            'price': {N: newProduct.price.toString()},
            'productPicUrl': {S: newProduct.picUrl},
            'userId': {S: newProduct.userId},
            'userName': { S: newProduct.userName}

        }

    };


    dynamo.putItem(productData, function(err) {

        if (err) {
            $log.error(err);
        } else {
            $log.info('Product Saved!!');
        }
    });

},

Our saveProductData function will accept an object as an input parameter. The next piece of code is essentially to generate a sort of Universally Unique Identifier (UUID) by concatenating userid and timestamp.

Next, we create our ProductData object in the format that DynamoDB can understand. As you can see, while passing each attribute field, we also need to define the data type for that attribute. The notation S stands for string and N stands for number.

The last piece of code calls the putItem method that will save this data in our DynamoDB table.

Creating the view for the add product form

To build out the view, create a file called add-products.html in the partials folder with the following code:

<h1>Add your Product</h1>
<hr/>

<form role="form" id="add-page" ng-submit="addProduct()">

    <div class="form-group">
        <label>Category</label>
        <select ng-model="newProduct.category">
            <option ng-repeat="category in categories">{{category}}</option>

        </select>
    </div>

    <div class="form-group">
        <label>Product Title</label>
        <input class="form-control" type="text" ng-model="newProduct.title" />
    </div>

    <div class="form-group">
        <label>Product Description</label>
        <textarea rows="8" class="form-control" type="text" ng-model="newProduct.description"></textarea>
    </div>

    <div class="form-group">
        <label>Product Price</label>
        <input class="form-control" type="number" ng-model="newProduct.price">
    </div>

    <div>
        <input type="submit" class="btn btn-success" value="List My Product">
    </div>

</form>

This is a regular form with fields to select a category, add a title, description, and price.

Building the controller for the add products view

Let’s create a controller called AddProductsCtrl in our controllers.js file, and add the following piece of code to it:

.controller('AddProductsCtrl', ['$scope', 'categoryService', 'authService', 'AWSservice',
    function($scope, categoryService, authService, AWSservice) {

        $scope.categories = categoryService.getCategories();

        $scope.newProduct = {};


        $scope.addProduct = function() {

            $scope.newProduct.userId = $scope.user.id;
            $scope.newProduct.userName = $scope.user.name;
            $scope.newProduct.picUrl = 'sw3/someURL';
AWSservice.saveProductData($scope.newProduct);

        }

    }
]);

The controller code is quite straightforward. We first populate the categories scope by calling the getCategoriesMethod of the categoriesService. Next, we capture some additional information such as the logged-in user’s ID and name, and push them into the newProduct object along with the other data that is coming in from the form.

You’ll also notice that we have a property called picUrl, where, for the time being, we are passing in a dummy value; we will change this once we get to our next section on uploading images.

This should be good for now. Save the files, and in the browser, navigate to http://localhost:8000/app/#/add and test out adding a couple of products.



You should receive a Product Saved message in your console on successful execution. Head over to the AWS management console, and verify that the data is saved in your DynamoDB table.

Uploading images to S3

Now, we’ll see how to upload the product pictures along with our add product form. We’ll start by adding the markup for file upload in the add-products.html partial:

<img width="250" ng-src="{{uploadedPicURL}}">
<div class="form-group">
<label>Product Picture</label>
<input class="form-control" type="file" accept="image/*" ng- model="newProduct.pic" onchange="angular.element(this).scope().uploadImage(this.files)">
</div>

Our intention is to start the file upload on to S3 as soon as a file is selected; hence, we use the onchange event. Again, from a usability standpoint, it makes sense to keep this piece of code at the top (preferably, after the category select box) so that after selecting the file, as the user is filling up the rest of the form, the image would have already been uploaded into S3, assuming that the file being uploaded is not very large in size.

We also have an img tag that will show the preview of the uploaded picture. Next, we will write our uploadImage method within the AddProductsCtrl controller:

$scope.uploadImage = function(files) {
    AWSservice.uploadPic(files).then(
        function(data) {
          
            $scope.newProduct.picUrl = data;
            $scope.uploadedPicURL = "https://s3.amazonaws.com/garage-commerce/" + data;
        }, function(err) {
            $log.error(err);
        })
}

The controller is quite simple; it takes the files object as an input argument and passes it to the factory function, waits for the promise to resolve, and sets the filename and image path in the scope properties. You would ideally want to store the S3bucket name in a scope property and use it to dynamically build the uploadedPic URL.

Tip

Don’t forget to remove the dummy picURL value that we were passing earlier in the addProduct method.

Next, we will work on the crucial piece, the factory service function that will do the job of uploading that file into S3.

We are going to create our function called uploadPic within the AWSservice factory and put in this following piece of code:

uploadPic: function(files) {
    var d = $q.defer();
    var file = files[0];
    var data = {
        Key: file.name,
        Body: file,
        ContentType: file.type
    };
    s3bucket.putObject(data, function(err, data) {
        var fileName = file.name;
        d.resolve(fileName);

        if (err) {
            d.reject(err);
            $log.error(err);
        } else {
            $log.info('successfully uploaded');
        }
    });
    return d.promise;
},

We first create an instance of our S3 object; then, we capture the file data in a data object. Using the S3.putObject method, we upload the data into S3. As we do not know how much time it would take for the file to upload, we set up a promise so that we get the callback once the file is successfully uploaded.

Tip

To keep things simple, we are uploading the image with the original filename. However, for a production-level setup, you might want to rename the files with some kind of a UUID to avoid overwrites.

Test out the add products form, and make sure that the images are getting uploaded on S3 and the data is being saved in the database. Go ahead and add a couple of products by selecting different categories.

Fetching the products lists for a category

The next step is to work on our product listing pages. The idea is when the user selects a category from the navigation bar, we show them the list of products belonging to that category.

DynamoDB provides two methods to fetch a group of listing:

  • Scan: This operation runs through every record in the database and returns a result set that matches the comparison parameters
  • Query: This operation, on the other hand, will find items or rows only using the primary key values; they can be hash key or range key values

Both query and scan operations return a maximum of 1 MB of data. In our case, we will use the scan operation along with ScanFilter to get the matching records for a given category.

We’ll head to our AWSservice factory and create a function called getProductsByCategory with the following code:

getProductsByCategory: function(category) {
    var d = $q.defer();
    var params = {
        'Limit': 100,
        'ScanFilter': {
            category: {
                AttributeValueList: [{
                    S: category
                }],
                ComparisonOperator: 'EQ'
            }
        }
    };
    dynamo.scan(params, function(err, data) {
        if (data) {
            d.resolve(data);
        } else if (err) {
            $log.error(err);
        }
    });
    return d.promise;
},

The piece of code that is interesting to look at is the formation of the params object, where we are setting up ScanFilter. The syntax of ScanFilter isn’t quite straightforward as you might have seen earlier.

We need to pass attributeName or field name on which you want to set the filter, then we pass the attribute’s value that we need to compare, and finally, we set the comparison operator. As we need to show results for the selected category, we use the EQ operator. The other operators that the ComparisionOperator accepts are as follows:

  • NE | LE | LT | GE | GT | NOT_NULL | NULL | CONTAINS | NOT_CONTAINS | BEGINS_WITH | IN | BETWEEN

Now, we’ll get to call our factory function, within the ProductsCtrl controller:

AWSservice.getProductsByCategory($scope.category).then(
    function(data) {
        $scope.productsListing = data.Items;
    })

Save the file, and refresh your page. You may get an error that says No credentials to load or something to that effect on the developer console. Ignore it for now. Wait for a few seconds, and try clicking on any of the other categories from the navigation bar. You should get to see the products getting displayed with funny extensions such as .S and .N appended to the products titles and price values. We’ll get to this later.

The reason why we get the No credentials message is because the request to the getProductsByCategory method gets fired before our AWS authentications and initialization takes place.




Using resolves to preload data

So, the question we have at hand is how to make sure that our Facebook authentication and AWS initialization take place before our productsCtrl and its methods are executed. angularjs provides a nifty little solution called resolve, which is available as a part of both the UI-Router and ngRoute modules.

Resolve will let you execute functions and inject the resolved data into the route’s controller. We can also create nested resolves and use the same method in the following example.

Let’s go ahead and set up the resolve. Resolves are set within the StateProvider, so we will add the following code in our app.js file as highlighted. We will modify our category state as follows:

$stateProvider.state('category', {
    url: '/:category',
    templateUrl: 'partials/products.html',
    controller: 'ProductsCtrl',
    resolve: {
        Facebook: 'Facebook',
        FBtoken: function(Facebook) {
            
            return Facebook.getLoginStatus(function(response) {
                if (response.status == 'connected') {
                    return response.token;
                }
            })
        }
    },
});

Resolve takes in an object that needs to be in the form of a key-value pair. The dependencies need to be defined as a key, and the factory function that needs to be resolved is the value of the key-value pair.

The preceding FBtoken function will resolve and return the access token. We now need to create our nested resolve that will take this access token and authenticate our AWS objects. We do this in the following manner:

AWSinit: function(FBtoken, AWSservice) {
    var token = FBtoken.authResponse.accessToken;
    return AWSservice.initializeAWS(token).$promise;
   
}

Now, as we refactored our code to initialize our AWS objects in the resolve, we no longer need to do it again from within our AppCtrlm. So, go ahead and remove the AWSservice.initializeAWS call from within the AppCtrl controller function.

Save the files, hit one of the categories URL, and ensure that the products are showing up without any errors on the console. Next, we’ll get rid of the additional S and N notations that get added to the end of the titles and price.

If you log the response of the getProductsByCategory method, you’ll notice that AWS is adding these S and N notations to the values to denote strings or numbers. To get rid of these is quite simple. We’ll simply modify our products.html partial to append these values to our expressions as follows:

<h2><a href="#/{{category%20+'/'+product.product_id.S}}">{{product.title.S}}</a> </h2>
<h5>{{product.price.N |currency}}</h5>
<p><i>-by:{{product.userName.S}}</i></p>