Building A food Order system Part 11

in #utopian-io7 years ago (edited)

Contribution Repository

Angular Github Repository

previously on this series, we were able to pass data from our route on the navbar component into the order component and display the the items from the server with the *ngFor directive, we also worked on adding items to our cartService, which in turns updates the Navbar component with the total amount of items in the cart. In this series, i am going to explain a lot about state management and how we can implement the same state on two separate component.

Rating

This tutorial is rated Intermediate.

What Will I learn?

  • We are going to learn how to use the object spread operator to mutate our objects and add a new property
  • We would learn how to use the map and filter functions to manipulate array of data.
  • Event bubbling on components
  • Lastly, we are going to learn more on observables and using them increment and decrement an objects property.

Requirement.

This series implementation

gifImage.gif

Introduction

Illustration for the tutorial

Untitled-1.jpg

Welcome back to our new series on the food App, yeah we have been doing well, below is the last state or structure o our application.

  • Voltron
    • node_modules/
    • src/
      • app
        • app-navbar/
        • item/
        • order
        • order-item
        • items-feed/
        • signup/
        • login/
        • app-routing.module
        • app.component.html
        • app.component.sass
        • app.component.ts
        • app.module.ts
        • auth.service.ts
        • item.service
        • cache.service
        • cart.service
      • asset/
      • environments/
      • item.ts
      • user.ts
      • auth.ts
    • .angular-cli.json
    • .editorconfig
      +.....

Recall in our last series, we able to implement the removal of items from the cartItems using a filter function by removing the selected items from the observable.
In this series we are going to refactor our code a little, what if we like our user to add, remove, increment and decrement quantity from the cart. Lets say we want to implement it in such a way that the user can carryout both functions in the items feeds and in the and in the order feed.

deleting.gif
previous implementation

This implementation is quite easy lets go on a smooth ride, a little bumpy so follow every step.
We are going to be using a lot of Vanilla JavaScript methods of which i will try to explain every code block.
let ride like the happy guy below.

1 Ma-vB61c-MeBPbJXvUUpGw.gif

We would take each module and work on it. lets start with the following Implementation on the Item feed component

  1. Adding the item to the cart
  2. Removing the item from the cart
  3. Increasing the quantity of items in the cart from the item feed
  4. decreasing the quantity of item in the cart

Lets Jump in.

When the item is fetch from the server using the item service we do not have a quantity property on the object

console.png

Before we implement the above, we need to add the quantity property to the items once they arrive from the server. We are going to harness the power of the object spread operator. In the previous tutorial, the items were fetched the getItems method which calls a similar method on the itemService to fetch the items from the server.

Open item-feed.component.ts and update the getItems method

getItems(): void {
    this.itemService.getItems()
        .subscribe(items => {
          this.items = items['result'].map(item => {
            return {
              ...item,
              qty: 0
            }  
          })
            
        });
  }

update.png

As explained before, we subscribe for the items from the cartServices' getItems method which returns the items from the server.
next up we added the qty property to item by iterating through the result of the items from the server and spreading the object using the object spread operator (...) to add the former object (item) and a new property of qty set to zero.

update.png

Lets update our item.component.html and the item.component.sass to support our new feature

<div class="order-item mb-2" *ngIf="item">
        <div class="item-photo">
            <a routerLink="/items/item-detail/{{item._id}}">gtimg  class="img-responsive" src="{{item.photo}}" alt=""></a>
        </div>
        
        <div class="details">
            <p class="detail-title">{{item.title}}</p>
            <p class="text-success">{{item.price}}</p>        
        </div>
        <div class="controls">
            <button   class="btn add-button-lg button-lg"><i class="fa text-success fa-plus"></i></button>
            <button  class="btn add-button"><i class="fa text-success fa-plus"></i></button>
            <span  >{{item.qty}}</span>
            g 
            
            <button   class="btn reduce-button"><i class="fa fa-minus"></i></button>
            <button   class="btn reduce-button"><i class="fa fa-times"></i></button>
        </div>
</div>

This component is responsible for showing the item, we are creating four buttons for the actions below

  • removing from cart
  • adding to cart
    +increasing and decreasing quantity
    later in the tutorial, the buttons would be toggle with a condition on the items' quantity property

update the item.component.sass with the styles.

.order-item 
    display: flex;
    flex: 1 1 auto;
    background-color: #eefefe;
    padding: 15px;
    border-radius: 10%;
    

.item-photo
    display: flex;
    height: 100px;
    width: 100px;

img
    border-radius: 20%;
    height: 100%;

.details
    display: flex;
    margin-left: 100px;
    flex-direction: column;
    justify-content: space-between;

.controls
    display: flex;
    margin-left: auto;
    flex-direction: column;
    align-items: center
    justify-content: space-around;

.detail-title
    font-weight: bolder;


button
    background-color: #fff;
    border-radius: 50%;
    padding: 2px;
.add-button
    border: 1px solid #28a745  

.reduce-button
    border: 1px solid #ff0039

.remove-button
    border: 1px solid #ff7171  
.fa
    font-size: 15px;
.fa-minus
    color:  #ff7171;
.fa-times
    color:  #ff7171;
.add-button-lg
    border: 1px solid #28a745

.button-lg
    background-color: #fff;
    border-radius: 0;
    padding: 5px;


.fa-plus:hover 
    color: #fff;

Emitting events for the buttons

Open items.component.ts where we imported EventEmitter and output, all we need to do is create some set of events using the EventEmitter and output.

@Output() onAddItemToCart = new EventEmitter<object>();

  @Output() onDecrementItemFromCart = new EventEmitter<object>();

  @Output() onRemoveItemFromCart = new EventEmitter<object>(); 

  @Output() onIncrementItemQuantity = new EventEmitter<object>(); 

Here we are creating new events, we set onAddItemToCart, onDecrementItemFromCart, onRemoveItemFromCart and onIncrementItemQuantity to new events just like a click event.

These events would be bubbled out to the parent container through the Output, we need this event to bubble out the items so we need to create the methods to bubble out the event once they are called by a click handle.

addItemToCart (item) {
    return this.onAddItemToCart.emit(item);
    
  }

decrementItemFromCart(item) {
    return this.onDecrementItemFromCart.emit(item);
  }

removeItemFromCart(item) {
    return this.onRemoveItemFromCart.emit(item);
  }


incrementItemQuantity(item) {
    return this.onIncrementItemQuantity.emit(item);
    
  }

The above methods return an emission of the item object once called by the click handler on a button.

Adding the click events to the buttons

Earlier, we created for sets up button on the Item.component. html, we need to add a click handle that calls the method on the component to emit each events.
eg (click)="someMethod()"

update the item.component.html to the code below


<div class="order-item mb-2" *ngIf="item">
        <div class="item-photo">
            <a routerLink="/items/item-detail/{{item._id}}">gtimg  class="img-responsive" src="{{item.photo}}" alt=""></a>
        </div>
        
        <div class="details">
            <p class="detail-title">{{item.title}}</p>
            <p class="text-success">{{item.price}}</p>        
        </div>
        <div class="controls">
            <button *ngIf="item.qty == 0" (click)="addItemToCart(item)" class="btn add-button-lg button-lg"><i class="fa text-success fa-plus"></i></button>
            <button *ngIf="item.qty > 0" (click)="incrementItemQuantity(item)" class="btn add-button"><i class="fa text-success fa-plus"></i></button>
            <span  *ngIf="item.qty > 1">{{item.qty}}</span>
            
            <button *ngIf="item.qty > 1" (click)="decrementItemFromCart(item)" class="btn reduce-button"><i class="fa fa-minus"></i></button>
            <button *ngIf="item.qty == 1" (click)="removeItemFromCart(item)" class="btn reduce-button"><i class="fa fa-times"></i></button>
        </div>

</div>

In the above, we are creating the item layout which contains the item photo, price and title shown by using interpolation

button.png

Next up, we need to create the methods to carryout these actions and bind them to the item-feed.component iteration.

Writing the methods to mutate objects in an observable

The only way our application can know the state of an object has change is by mutating the object from a stream that is monitored by the application, We talked about observables in previous tutorial were we said observables are streams of data monitored by the application.

In this case, we want to mutate the the state of the item in the cart. We already have the cartItems in the cartService as an observable, lets write methods to mutate the state.

Open up the cartService.ts

In the last series, we added methods to add and remove from the observable, lets create two methods to increment and decrement the quantity of item in the observable

announceCartItemDecrement (item) {
      item.qty = item.qty - 1;
      return this.cartAnnouncerSource.next(this.cartItems);
  }


  announceIncrementItemQuantity (item) {

    item.qty = item.qty + 1;
    
    return this.cartAnnouncerSource.next(this.cartItems);
  }

The first method, accepts the item, and retrieves the property qty and decrement it by running
item.qty = item.qty - 1; when ever it is called. The last line announces to the observable that a data has change.

The second method is the reverse of the first which increases the qty when ever it is called and announce to the observable that the data has changed

button.png

Next up we need to use these methods in our component
We are going to refactor our code differently from the last time, "what is the mission?", we want to be able to add a particular item once into the cart and we wont be able to add it if its already exist and we also want to be able to increase the quantity and also decrease it.

Lets create a method to check if an item exist in the cart
Note this method return an object

getItemInCart (cartItem) {
    return this.cartService.cartItems.filter(item => item['_id'] === cartItem['_id'])[0]
  }

The method filter through the observable and returns the item that matches the one in the cart. The notation "[0]" tells the filter method to return the first object in the array. To learn more on the filter method checkout this article

The next methods checks if the getItemInCart method evaluates to undefined, if yes it means the said item doesn't exist in the cartItem then it run the method to increment the qty and pushes the item to the observable.

onAddItemToCart (item) {


    if (!this.getItemInCart(item)){
      this.onIncrementItemQuantity(item)
      this.cartService.cartItems.push(item);
      this.cartService.announceCartItem(item);
      this.cartItems = this.cartService.cartItems;
      

    }
  }

Note we haven't written the method to increment and decrement from the observable through the item feed component

onDecrementItemFromCart(item: Item) {
    this.cartService.announceCartItemDecrement(item);
    
  }

  onIncrementItemQuantity(item) {  
    this.cartService.announceIncrementItemQuantity(item);
    

  }

The both methods calls the method in the service to decrement and increment the quantity on the observable respectively by announcing to the source that the object has changed.

Finally lets update the onRemoveItemFromCart()

onRemoveItemFromCart(item) {
    this.cartService.announceCartItemRemoval(item);
    this.onDecrementItemFromCart(item)
    
  }

When the method is clicked, we need to call the OnDecremenItemFromCart method to reduce the quantity in the cart zero before removing from the cart.

Binding the to event listeners on the Item-feed.

The event listeners picks up any bubbled events from the child component, so need to make the binding to listen for those events from the child.

Open up item-feed.component.html and add the code below.

<app-item [item]="item"
            *ngFor="let item of items" 
            (onRemoveItemFromCart)="onRemoveItemFromCart($event)"
            (onIncrementItemQuantity)="onIncrementItemQuantity($event)"
            (onDecrementItemFromCart)="onDecrementItemFromCart($event)" 
            (onIncrementItemFromCart)="onIncrementItemFromCart($event)"
            (onAddItemToCart)="onAddItemToCart($event)" >
        </app-item>

binding.png

Rendering buttons by conditions on the quantity

We want to show the add cart button when the items are loaded the toggle the display of the button. When the item is in the cart, the qty property would be set to "1" and the the decrement and increment buttons are showed. The decrement button is replaced with the removeItemFromcart button;

What are the conditions to show the different buttons?
qty = 0, show the ad button
qty = 1, show the remove and increment button
qty > 1, show the decrement button

Update the item.component.html with the new (*ngIf) directives

<div class="controls">
            <button *ngIf="item.qty == 0" 
                (click)="addItemToCart(item)" 
                class="btn add-button-lg button-lg">
                <i class="fa text-success fa-plus"></i>
            </button>
            <button *ngIf="item.qty > 0" 
                (click)="incrementItemQuantity(item)" 
                class="btn add-button"><i class="fa text-success fa-plus"></i>
            </button>
            <span  *ngIf="item.qty > 1">{{item.qty}}</span>
            <button *ngIf="item.qty > 1" (click)="decrementItemFromCart(item)"
                 class="btn reduce-button"><i class="fa fa-minus"></i>
            </button>
            <button *ngIf="item.qty == 1" (click)="removeItemFromCart(item)"
                class="btn reduce-button"><i class="fa fa-times"></i>
            </button>
        </div>

directive.png

Updating the Order and the Order-item component

The order item is just like the item component, it would bubble out event to listen for the onRemoveItemFromCart, onIncrementQantity and onDecrementQantity, just like the item.component.ts, we need to create and emit new events

@Output() onDecrementItemFromCart = new EventEmitter<object>();

  @Output() onRemoveItemFromCart = new EventEmitter<object>(); 

  @Output() onIncrementItemQuantity = new EventEmitter<object>();

The above are set to new events which are bubble by a method we are yet to create, lets create those method

decrementItemFromCart(cartItem) {
    return this.onDecrementItemFromCart.emit(cartItem);
  }

  removeItemFromCart(cartItem) {
    return this.onRemoveItemFromCart.emit(cartItem);
  }


  incrementItemQuantity(cartItem) {
    return this.onIncrementItemQuantity.emit(cartItem);
    
  }

The method once called, bubble out the cartItem that was clicked to the parent container which is the order.component.ts
Next up we need to add the buttons and the methods just like in the item component to call these
methods to bubble the events.

update the order-item.component.html


<div class="order-item mb-2" *ngIf="cartItem">
    <div class="item-photo">
        gtimg src="{{cartItem.photo}}" alt="">
    </div>
    
    <div class="details">
        <p class="detail-title">{{cartItem.title}}</p>
        <p class="text-success">{{cartItem.price}}</p>        
    </div>
    <div class="controls">
        <button (click)="incrementItemQuantity(cartItem)" class="btn add-button"><i class="fa text-success fa-plus"></i></button>
        <span >{{cartItem.qty}}</span>
        <button (click)="decrementItemFromCart(cartItem)" class="btn reduce-button"><i class="fa fa-minus"></i></button>
        <button  (click)="removeItemFromCart(cartItem)" class="btn reduce-button"><i class="fa fa-times"></i></button>
    </div>
</div>

Above, we added three buttons to to call the methods to increment, decrement quantities and remove the item from cart

update the item.component.html with its styles to show the structure of its layout.

.order-item 
    display: flex;
    flex: 1 1 auto;
    background-color: #eefefe;
    padding: 15px;
    border-radius: 10%;
    

.item-photo
    display: flex;
    height: 100px;
    width: 100px;

img
    border-radius: 20%;
    height: 100%;

.details
    display: flex;
    margin-left: 60px;
    flex-direction: column;
    justify-content: space-between;

.controls
    display: flex;
    margin-left: auto;
    flex-direction: column;
    align-items: center
    justify-content: space-around;

.detail-title
    font-weight: bolder;


button
    background-color: #fff;
    border-radius: 50%;
    padding: 2px;
.add-button
    border: 1px solid #28a745  

.reduce-button
    border: 1px solid #ff0039

.remove-button
    border: 1px solid #ff7171  
.fa
    font-size: 15px;
.fa-minus
    color:  #ff7171;
.fa-times
    color:  #ff7171;

Creating the methods for which the are called when the events are propagated by the child component

In the previous section, we created methods that emits events once they are called by the click event on the button in the order component.
On the item-feed.component.ts, the methods to carryout the actions would be created

onDecrementItemFromCart(cartItem: Item) {
    this.cartService.announceCartItemDecrement(cartItem);
    this.getOrderItems()
  }

This method, calls the announceCartItemDecrement method on the cartService which accepts a cartItem and reduces the cartItems' quantity property by 1.

onIncrementItemQuantity(cartItem: Item) {
    this.cartService.announceIncrementItemQuantity(cartItem);
  }

This method, calls the announceIncrementItemQuantity method on the cartService which accepts a cartItem and Increases the cartItems' quantity property by 1.

onRemoveItemFromCart(cartItem: Item) {
    this.cartService.announceCartItemRemoval(cartItem);
    this.cartItems = this.cartService.cartItems
  }

This method calls the announceCartItemRemoval method on the cart which accepts a cartItem and
using the filter method check if the currentItems that are been filtered is not equal to the cartItem

currentItem => currentItem !== item

Binding the methods to event on the order component

We have to bind the methods to the iteration on the parent component t listen for when the child emits an event. Update order.component.html to reflect the binding

<app-order-item [cartItem]="cartItem" 
          *ngFor="let cartItem of cartItems" 
          (onRemoveItemFromCart)="onRemoveItemFromCart($event)"
           (onIncrementItemQuantity)="onIncrementItemQuantity($event)" 
           (onDecrementItemFromCart)="onDecrementItemFromCart($event)">
 </app-order-item>

Here we are simply binding the events bubbled from the child component to the method to carryout the actions on the order.component.ts

Toggling button display on the Order-item.component.ts

Just like we did on the item.component.html, we need to toggle the display of the buttons using the *ngIf directive to render different button and the span element once a certain condition is met.

Update order-item.component.html

<div class="controls">
        <button (click)="incrementItemQuantity(cartItem)" class="btn add-button"><i class="fa text-success fa-plus"></i></button>
        <span *ngIf="cartItem.qty > 1">{{cartItem.qty}}</span>
        <button *ngIf="cartItem.qty > 1" (click)="decrementItemFromCart(cartItem)" class="btn reduce-button"><i class="fa fa-minus"></i></button>
        <button *ngIf="cartItem.qty === 1" (click)="removeItemFromCart(cartItem)" class="btn reduce-button"><i class="fa fa-times"></i></button>
    </div>

The button to decrement quantity is shown when the cartItem.qty propertie is > 1 and the button to remove the item from cart is only displayed when the cartItem.qty is equal to 1.

Final effect
gifdisplay.gif

We have a little problem from our application, if you watch the gif above after putting the items on cart and visiting the order route, if you return back to the initial route, events carried out on the item feed disappears. How do we solve this?

We need to find a way to retrieve the values mutated in the cart items' observable and displaying the mutated values in the cart on the item feed. Lets write a method to do this.

 compareFeedItems () {
    let self = this;
    return this.items.length ? 

      this.items.map(item => {
        return self.getItemInCart(item) ? self.getItemInCart(item) : item;
      }) : 
      this.items;

  }

Here, this method says if there are items on the item feed, when the check returns true, iterate through the available items and check using the getItemInCart method to know if a particular item is in the cart, when the statement evaluate to true, return the item in cart else return all items.

Since this method will return every thing on the item-feed, we can up date our item.component.html to use the method for iteration.

*ngFor="let item of compareFeedItems()

gifImage.gif

Conclusion

In this series, we where able to remove, increment and decrement quantities of items from the observable and we where also able to maintain equal state in the item-feed and the order-feed component. next up we would run calculations on our order and take about skeletal Screen loading in angular.
Remember to check out the Food Order System Repo

Curriculum

Resources

Sort:  

Congratulations @sirfreeman! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of upvotes
Award for the total payout received

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Do you like SteemitBoard's project? Then Vote for its witness and get one more award!

Thank you for your contribution.
While I liked the content of your contribution, I would still like to extend few advices for your upcoming contributions:

  • Resources: Put more extra resources.
  • Screenshots: Provide better quality, clearer, and more professional screenshots
  • Structure of the tutorial: Improve the structure of the tutorial.

Looking forward to your upcoming tutorials.

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Hey @sirfreeman
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Contributing on Utopian
Learn how to contribute on our website or by watching this tutorial on Youtube.

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!