Sometimes, you need a progress bar that just looks cool. This is one of those times.

Well I assume it's one of those times or you wouldn't be here.

So I'm going to show you how to develop a Salesforce-like progress bar with arrows. 

And yes, it looks cool:

 

That's what you're talking about, isn't it? 

That's what you're talking about!

I'll show you how to do that here. You can follow along or go straight to the source.

Remember: if you're having problems running the Angular app locally, be sure to check the README.

Please note: this guide is a part of an ongoing series of tutorials on how to create a customer relationship management (CRM) application from scratch. The GitHub code for each guide belongs to a specific branch. The master branch holds the entire application up to the current date. If you want to follow the whole series, just view the careydevelopmentcrm tag. Note that the website displays the guides in reverse chronological order so if you want to start from the beginning, go to the last page.

The Business Requirements

Your boss Smithers walks into your office eating a sandwich.

"It's about that CRM app you're working on," he says while chewing his sandwich. With his mouth open.

"You need to make it easy for sales reps to advance the status of a contact. Give them something like that Salesforce progress bar with the pointy things on the end of each box. Yeah, I like that."

He walks out of your office while still chewing that same bite of sandwich.

A New Component

This progress bar feels like something that might be reusable. Therefore, you should make it a new component.

Head to your command line and type the following:

ng g c features/contacts/status-progress-bar

Then, head back into your IDE to add the necessary code.

The Necessary Code

Start by adding some styling. That's the most important part here if you're looking for that cool progress bar.

Add this to styles.css:

.progress-div {
  box-sizing: border-box;
  padding-bottom: 15px;
}

.progress {
  padding: 0;
  list-style-type: none;
  font-family: arial;
  font-size: 12px;
  clear: both;
  line-height: 1em;
  margin: 0 -1px;
  text-align: center;
}

.progress li {
  float: left;
  padding: 10px 30px 10px 40px;
  background: #3F51B5;
  color: #fff;
  position: relative;
  width: 10%;
  margin: 0 1px;
}

.progress li:before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
}

.progress li.not-first:before {
  border-left: 16px solid #fff;
  border-top: 16px solid transparent;
  border-bottom: 16px solid transparent;
}

.progress li:after {
  content: '';
  border-left: 16px solid #3F51B5;
  border-top: 16px solid transparent;
  border-bottom: 16px solid transparent;
  position: absolute;
  top: 0;
  left: 100%;
  z-index: 20;
}

.progress li.inactive {
  background: #555;
  cursor: pointer;
}

.progress li.inactive:hover {
  background: #33B0FF;
}

.progress li.inactive:after {
  border-left-color: #555;
}

.progress li.inactive:hover::after {
  border-left-color: #33B0FF;
}

.progress li.completed {
  width: 2% !important;
}

.progress .active-status {
  font-weight:bold;
}

I won't get into all of that in nauseating detail so let me just cover the important points:

  • Each "box" in the progress bar gets displayed as an unordered list. That's why you see a lot of styling for <li> elements up there.
  • Only the first "box" in the progress bar is closed on the left-hand side. That's why there's special styling for the not-first class. 
  • The inactive class is used for statuses ahead of the current status. If you go back to the image above, they're the statuses colored grey instead of blue. 
  • When the user hovers over a "box," it changes color. So you see some hover selectors in the code above.

By the way, it's a great idea to put that in styles.css rather than in the component-specific CSS file because that really looks like the kind of thing you might need in other components.

Coding the Component

Now it's time to add some code to that component you just created. Edit status-progress-bar.component.ts and make it look like this:

@Component({
  selector: 'app-status-progress-bar',
  templateUrl: './status-progress-bar.component.html',
  styleUrls: ['./status-progress-bar.component.css']
})
export class StatusProgressBarComponent implements OnInit {

  @Input() contact: Contact;

  availableContactStatuses: DropdownOption[] = contactStatuses;
  contactStatusIndex: number;

  constructor(private contactService: ContactService, private alertService: AlertService) { }

  ngOnInit(): void {
    this.setContactStatusIndex();
  }

  private setContactStatusIndex() {
    if (this.contact) {
      this.contactStatusIndex = this.availableContactStatuses.findIndex(status => this.contact.status === status.value);
    }
  }

  updateStatus(index: number) {
    if (index > this.contactStatusIndex) {
      this.alertService.clear();

      let newStatus: DropdownOption = this.availableContactStatuses[index];
      this.contact.status = newStatus.value;

      this.contactService.update(this.contact)
        .subscribe(
          (contact: Contact) => this.handleContactSaveResponse(contact),
          err => this.handleContactSaveError(err)
        );
    }
  }

  handleContactSaveResponse(contact: Contact) {
    this.setContactStatusIndex();
    this.alertService.success("Contact status updated!");
  }

  handleContactSaveError(err: Error) {
    console.error(err);
    this.alertService.error("Problem updating contact status!");
  }
}

For starters, the class accepts a Contact object as an input from the parent component. You can see that in the @Input() line towards the top.

Next, the component keeps an array of available contact statuses. As you can see, the code hijacks the DropdownOption class that maps names and values (like "Under Review" to "UNDER_REVIEW"). It's better than duplicating code.

The contactStatusIndex variable identifies which index in the array of statuses represents the contact's current status. It's using 0-based indexing so the first element is element #0.

Speaking of that variable, the setContactStatusIndex() method sets it. It does that with the aid of the findIndex() method that you can apply to any array.

The updateStatus() method gets called when the user clicks a non-active status on the screen. That sets the contact's status to whatever the user clicked.

That method also calls the update() method on ContactService. If the update is successful, the code goes to handleContactSaveResponse() where it updates contactStatusIndex and prints an alert that says the status was updated successfully.

If there was an error, the code will post an error alert that says something went wrong.

Making It With Markup

Next, edit status-progress-bar.component.html. Make it look like this:

<div class="progress-div">
  <ul class="progress">
    <li *ngFor="let status of availableContactStatuses; index as i; first as isFirst"
        [class.inactive]="i > contactStatusIndex"
        [class.completed]="i < contactStatusIndex"
        [class.not-first]="!isFirst"
        (click)="updateStatus(i)">
      <span *ngIf="i >= contactStatusIndex" [class.active-status]="i == contactStatusIndex">{{status.display}}</span>
      <span *ngIf="i < contactStatusIndex">&#10003;</span>
    </li>
  </ul>
</div>

There's the code that displays the status bar.

It's all wrapped neatly in a <div> that uses a class you saw defined in styles.css.

Inside that <div> is an unordered list as I mentioned earlier. Each <li> element in the list represents a different status.

The markup uses the *ngFor structural directive to spit out one <li> element for each status in the availableContactStatuses array that you saw in the last section. It also keeps track of the index in each iteration as well as the first element.

Then it adds classes as needed programatically.

First, the markup adds the inactive class if the current index in the iteration is greater than the selected index. That will display the current status as grey instead of that pretty blue color.

Next, the markup adds the completed class if the current index in the iteration is less than the selected index. That shrinks the width of the status "box" significantly. 

Statuses that happened "in the past" don't need to show the full status name. Instead, they just show a checkmark.

Next, the markup adds the not-first class if the status is not the first one in the list. That's necessary so the first status doesn't have a "tail." It will just have a flat left side like any rectangle.

Finally, that <li> element includes a (click) event handler that runs the updateStatus() method you saw in the previous section.

Inside the list item, the markup includes a couple of <span> tags.

The first one displays if the current index is more than or equal to the selected index. In that case, the markup displays the full name of the status (like "Contacted").

Otherwise, the markup displays a checkmark with the second <span>.

Just Add Progress

Now all that's left to do is include that component in view-contact.component.html.

<div *ngIf="contact" style="margin-bottom: 15px" fxHide.lt-md="true">
  <app-status-progress-bar [contact]="contact"></app-status-progress-bar>
</div>

The <app-status-progress-bar> element stays inside a <div> element that only displays once the component loads the Contact object.

Then, it passes that object to the child component where it's received via @Input().

And that's it. That's all you need to do to get your progress bar working.

Wrapping It Up

No need to do the "test it out" thing here since you can see what it looks like above. 

Now that you know how to make it happen, feel free to use that Salesforce-like progress bar in your own code. You can find plenty of uses for it even if you're not developing a CRM.

Of course, you will have to adapt the code you see here to suit your own requirements.

Speaking of code, feel free to grab it all on GitHub.

Have fun!

Photo by Leohoho on Unsplash