Searchable Bootstrap Accordion Using Angular

The Bootstrap collapse plugin enables some nice functionality, including the ability to create an accordion. In this quick tutorial I want to show you how you can create a way to filter, or search, the data in the accordion. I will be using Angular 1.5 and Bootstrap 3, as well as TypeScript. I will also be using a remote JSON file to create the accordion using the Angular UI Bootstrap directive: uib-accordion. Let’s get started!

Getting Started

To get started, you should be using:

  1. Angular 1
  2. Bootstrap 3
  3. Angular UI Bootstrap

I’m not going to cover how to install/configure these. This tutorial assumes that you already have a bootstrapped Angular 1 application that specifies the “ui.bootstrap” dependency for Angular UI Bootstrap. I’m also assuming that you have the Bootstrap CSS included.

tl;dr

Check out the plunker: https://plnkr.co/edit/Nzilmc97H3PpsBDvJQqa?p=preview

Implement uib-accordion

Next, you will want to implement the uib-accordion and uib-accordion-group directives. Here is what my template looks like:

<uib-accordion close-others="false">
  <div class="row">
    <div class="col-xs-12 col-lg-10 col-lg-offset-1">
      <div class="alert alert-warning" role="alert" ng-show="$ctrl.results.length === 0 && $ctrl.q.length > 0" ng-cloak>
 Sorry, no matches found for <strong>{{$ctrl.q}}</strong></div>
      <uib-accordion-group id="{{categoryGroup.id}}" class="list-group" ng-repeat="categoryGroup in $ctrl.categories" heading="{{categoryGroup.title}}" is-open="$ctrl.getGroupById(categoryGroup.id).open"> 
        <a id="{{categoryGroup.id}}_{{$index}}" href="" class="list-group-item" ng-repeat="category in categoryGroup.categories">
          {{category}} Classes
        </a>
      </uib-accordion-group>
    </div>
  </div>
</uib-accordion>

Some things to note:

  • I have a div.alert element at the top that is used as an indicator when you are searching and there are no results to display. I hide this by default using the ng-cloak directive.
  • The uib-accordion-group element uses an ng-repeat directive to iterate over the data in my JSON file (below).
  • The uib-accordion-group specifies the group heading; and it also specifies the is-open property. This property is called open and is part of the group object that is returned from the controller’s getGroupById() method.
  • The a.list-group-item element also uses an ng-repeat directive to iterate over the categories in each of the groups.
  • I have set id values on both the uib-accordion-group and a.list-group-item elements. We will be using those later on to reference the elements.

Here is a snippet from the categories.json file:

[{
 "title": "Adobe",
 "id": 5,
 "categories": ["Acrobat", "ActionScript", "After Effects", "Animate", "Captivate", "ColdFusion", "Contribute", "Dreamweaver", "FrameMaker", "FrameMaker (Structured)", "Illustrator", "InDesign", "LiveCycle", "Photoshop", "Premiere Pro", "Presenter", "RoboHelp"]
}, {
 "title": "Big Data",
 "id": 37,
 "categories": ["Actian", "Amazon Redshift", "Cognos", "Greenplum", "Hadoop", "IBM Netezza", "Kognitio", "Spark", "Teradata", "Vertica"]
}]

 

Add Search Input

Next, we will add the search input to our uib-accordion. To do this, I will create a new .row element that will contain the input field. I am also going to add some buttons that will enable the user to collapse all or expand all of the accordion groups. Here is what that looks like:

<div class="row">
  <div class="col-xs-12 col-sm-6 col-md-8 col-lg-7 col-lg-offset-1">
    <div class="form-group">
      <div class="input-group">
        <span class="input-group-addon">
          <i class="fa fa-search fa-fw"></i>
        </span>
        <input autocapitalize="off" autocomplete="off" autocorrect="off" class="form-control ng-scope" type="search" placeholder="find a course" ng-model="$ctrl.q" ng-model-options="{debounce: {'default': 500, 'blur': 0}}" />
        <span class="input-group-btn">
          <button type="reset" class="btn btn-default clear" ng-click="$ctrl.clear()">
            <i class="fa fa-times"></i>
            <span class="sr-only">Clear Search</span>
          </button>
        </span>
      </div>
    </div>
  </div>
  <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3 clearfix">
    <div class="btn-group pull-right" role="group">
      <button class="btn btn-default" ng-click="$ctrl.expandAll()">
        <i class="fa fa-arrow-circle-o-down"></i> Expand All
      </button>
      <button class="btn btn-default" ng-click="$ctrl.collapseAll()">
        <i class="fa fa-arrow-circle-o-up"></i> Collapse All
      </button>
    </div>
  </div>
</div>

Some notes:

  • The input[type="text"] element is binding to the q property in my controller ($ctrl) using the ng-model directive.
  • The input[type="text"] element uses the ng-model-options directive to specify a debounce of 500 milliseconds (ms). This prevents the search from firing after each letter is entered into the input, as it will wait 500 ms after the last key entry before triggering the search.
  • I have a button to clear the search that calls the clear() method in the controller.
  • The buttons to expand and collapse all of the accordion groups call the expandAll() and collapseAll() methods in my controller, respectively.

Create Category Service

This step is likely optional for your implementation, as you may already have the data for your accordion wired up. But, if you recall, we are going to fetch the data for the accordion in this example from a remote .json file. So, to do this, we’ll create a think service for retrieving the categories.

Also, remember that I am using TypeScript for the example. So you’ll notice that I am using an ES6 class to create the CategoryService.

export class CategoryService {
 
 public static MODULE_NAME = &quot;CategoryService&quot;
 
 public static bootstrap(app) {
   new CategoryService(app);
 }
 
 constructor(private app) {
   this.factories();
 }
 
 factories() {
   this.app.factory(CategoryService.MODULE_NAME, [&quot;$http&quot;, function($http) {
     return {
       query: function() {
         return $http.get(&quot;api/categories.json&quot;);
       }
     }];
   });
   return this;
 }
}

Some notes about the CategoryService class:

  • The static method bootstrap() creates a new instance of the CategoryService class.
  • The constructor fires off the factories() method, which creates the factory named CategoryService.
  • I inject the $http service and use this in my query() method to retrieve the categories.json file.

When we get to the controller code we will inject the CategoryService into our constructor method and then we will use the query() method to populate the $ctrl.categories property.

A Note About @types

At this point you might be asking why I am not specifying types for the IHttpService variable $http that is injected into the factory. Or why I did not specify a return type for the query() function, which should be a Promise object. Well, the issue is that Plunker doesn’t seem to support using types from the DefinitelyTyped library.

To fully appreciate the use of TypeScript, we really ought to be using TypeScript definition files for Angular. While I will not be specifying these types in this example to ensure that the files run on plunker, you probably should. To do this, just install the angular definition file via the npm package:

$ npm install @types/angular --save-dev

Create accordion Directive

The next step is to create our accordion directive. To do this, we’ll create a new file named accordion.directive.ts. The AccordionDirective class will require our AccordionController, which we have not yet created, but will soon. We will also need to add the directive to our template. First, let’s take a look at the AccordionDirective class.

export class AccordionDirective {
 
 public controller = "AccordionController";
 public controllerAs = "$ctrl";
 public restrict = "AE";
 
 public link = (scope, element, attributes, accordionController) => {
   console.log("[AccordionDirective.link] Initialializing AccordionController");
   accordionController.init(scope, element, attributes);
 };
 
 public static Factory() {
   return () => new AccordionDirective();
 }
 
}

Some notes:

  • Again, I am using TypeScript, so this might look a little different than your more traditional pure-vanilla-JS approach. But, stick with me.
  • As I previously discussed, I am not using the DefinitelyTyped interfaces for Angular, but if I were, this class would implement the ng.IDirective interface.
  • I have properties for controller, controllerAs, and restrict.
  • I have a public method named link() that uses the fat-arrow syntax to create a new function. The link method is passed the scope (IScope) object, the instance element (of type JQuery), the instance attributes (of type IAttributes) object, as well as an instance of my AccordionController class.
  • I call the init() method in the AccordionController object.
  • The static Factory() method creates a factory function, which creates a new instance of the AccordionDirective class.

Create accordion Module

I like to wire up module, directives, and controllers using an ES6 module. I know that this might not comply with John Papa’s style guide, but hey, using TypeScript and ES6 kicks butt. Why not take advantage of it, right?

So, let’s create a new Accordion module. Create a file named accordion.directive.ts:

import { AccordionDirective } from "./accordion.directive";

export class Accordion {
 
 public static MODULE_NAME = "accordion";
 
 private app;
 
 constructor() {
   this.create().directives();
 }
 
 public create(): Accordion {
   this.app = angular.module(Accordion.MODULE_NAME, ["ngAnimate", "ui.bootstrap"]);
   return this;
 }
 
 public directives(): Accordion {
   this.app.directive("accordion", AccordionDirective.Factory());
   return this;
 }
}

Let’s dive into this:

  • The Accordion class has a constructor method that fires off the create() and directives() methods.
  • We will add our directives and controllers in here later. The controllers will be created in a new controllers() method.
  • All of our methods return this. This enables us to chain them together easily.
  • I am using the angular-animate 1.5.8. I specify the “ngAnimate” module name in the array of required modules.
  • I also specify the “ui.bootstrap” module name to require Angular UI bootstrap.
  • Finally, I create the accordion directive using the AccordionDirective class that is imported on line 1.

IGroup and ISearchTerm

Next, let’s quickly create some interfaces and get ready to create the controller. I put the interfaces at the top of the controller file, but you could also put these into a separate file and import them in the controller. To start, create a new accordion.controller.ts file.

interface IGroup {
 hidden: boolean;
 id: string;
 open: boolean;
}

interface ISearchTerm {
 element: Element;
 groupId: string;
 hidden: boolean;
 id: string;
}

The IGroup interface has three properties:

  1. hidden – this will toggle the display of a uib-accordion-group element.
  2. id – this will store the id string of the uib-accordion-group element.
  3. open – this will toggle if the items within the uib-accordion-group element are displayed.

The ISearchTerm interface has four properties:

  1. element – the JQuery element for each accordion-search-term directive. We will be creating this new accordion-search-term directive shortly.
  2. groupId – the id value for the item’s uib-accordion-group element.
  3. hidden – this will toggle the display of the accordion-search-term element.
  4. id – the id string of the accordion-search-term element.

We will implement these interfaces in our controller, and we will also create two new directives: accordion-group and accordion-search-term.

The AccordionController

The AccordionController is the controller that we have been referencing all along, which is created as $ctrl.

export class AccordionController {
 
 public static $inject = ["CategoryService"];
 
 public categories = [];
 public groups: IGroup[] = [];
 public results: ISearchTerm[] = [];
 public searchTerms: ISearchTerm[] = [];
 private _q = "";
 
 constructor(private CategoryService: CategoryService) {
   console.log("[AccordionController:constructor] creating AccordionController instance.");
 }
 
 public init(private $scope, private $element, private $attributes) {
   this.CategoryService.query().then(result => this.categories = result.data);
   return this;
 }
 
 public get q(): string {
   return this._q;
 }
 
 public set q(q: string) {
   //store query
   this._q = q;

   //perform search
   this.search();
 }
}

Let’s walk through the start of our new AccordionController class:

  • First, I have a static property named $inject. This is an array of dependencies that Angular will inject into the constructor function. Here, we are injecting the CategoryService factory that we created previously.
  • Next, we will create some public properties that our template will be able to access:
    • categories – the categories.json data.
    • groups – an array of IGroup objects.
    • results – an array of ISearchTerm objects that match a query.
    • searchTerms – an array of ISearchTerm objects.
  • I also create a private instance variable named _q. The $ctrl.q property that we bound our input[type="text"] element to will be accessed and mutated via ES6 get and set methods – woot!
  • We use the CategoryService in the init() method that is invoked from the directive’s link() function. We execute the query() function that we defined. It retrieves the JSON data and returns a Promise object. We then map the result to the this.categories property for the AccordionController. If you recall, we referenced the $ctrl.categories property in our template’s ng-repeat directive.
  • The get q() function returns the value of the private _q instance variable.
  • The set q() method stores the value of the user’s query and then invokes the search() method. Every time the user modifies the value of the $ctrl.q property we will invoke the search() method to filter our accordion and to display the appropriate items that match based on the text value within the accordion-search-term element.

Let’s define the controller in our Accordion module:

import { AccordionController } from "./accordion.controller";

constructor() {
  this.create().controllers().directives();
}
 
public controllers() {
  this.app.controller("AccordionController", AccordionController);
  return this;
}

We simply create a new controller() method that creates the AccordionController and invoke this in our constructor function.

Create accordion-group Directive

Now let’s create the accordion-group directive, which will be applied to the uib-accordion-group element. This directive will simply invoke the addGroup() method in our controller, which will create a new IGroup object.

Create a new file named accordion.group.directive.ts and create (and export) the AccordionGroupDirective class. This will look very similar to our AccordionDirective class that we created previously.

export class AccordionGroupDirective {
 
 public require: string = "^accordion";
 public restrict = "AE";
 
 public link = (scope, element, attributes, accordionController) => {
   let groupId = attributes.id;
   accordionController.addGroup(groupId);
 };
 
 public static Factory() {
   return () => new AccordionGroupDirective();
 }
 
}

As I said, this is not much different than our AccordionController, but there are a couple of changes to note:

  • We require the accordion directive by setting the value of the require property to “^accordion”.
  • The addGroup() method on the AccordionController object is invoked, passing in the id attribute string value.

Let’s add the addGroup() method to the AccordionController class:

public addGroup(id: string, open: boolean = false): AccordionController {
  //create group
  let group: IGroup = {
    hidden: false,
    id: id,
    open: open
  };

  //append group
  this.groups.push(group);
  
  return this;
}

The addGroup() method creates a new IGroup object and appends it to the this.groups array.

Next, we need to specify the directive in our Accordion module:

import { AccordionGroupDirective } from "./accordion.group.directive";

public directives(): Accordion {
  this.app.directive("accordion", AccordionDirective.Factory());
  this.app.directive("accordionGroup", AccordionGroupDirective.Factory());
  return this;
}

We import the AccordionGroupDirective class and then create a new directive with the normalized named of “accordionGroup”.

Now we need to modify our template to include the accordion-group directive on the uib-accordion-group element:

<uib-accordion-group 
  id="{{categoryGroup.id}}"
  class="list-group"
  ng-repeat="categoryGroup in $ctrl.categories"
  heading="{{categoryGroup.title}}"
  is-open="$ctrl.getGroupById(categoryGroup.id).open"
  ng-hide="$ctrl.isGroupHiddenWithId(categoryGroup.id)"
  accordion-group ng-cloak
>

Here is a quick rundown on the attributes:

  • id – the unique id from the categories.json file.
  • class – Bootstrap’s class for the list-group-item.
  • ng-repeat – iterating over the array of categories.
  • heading – the group heading value, which is in the title property of the categories.json file.
  • is-open – the open property of an IGroup object.
  • ng-hide – invokes a new method called isGroupHiddenWithId() that we will add to the controller.
  • accordion-group – the directive.
  • ng-cloak – hide the group.

So, let’s go back to our controller and add the new methods for the accordion-group directive:

public getGroupById(id: string): IGroup {
  for (let group of this.groups) {
    if (group.id.toString() === id.toString()) {
      return group;
    }
  }

  //create an empty group stub
  let group: IGroup = {
    hidden: false,
    id: null,
    open: false
  };

  return group;
}

public isGroupHiddenWithId(id: string): boolean {
  return this.getGroupById(id).hidden;
}

The getGroupById() group returns the IGroup object that matches the id string specified. If one cannot be found, we simply create a stub and return thisthis is only used briefly between the time that the application is bootstrapped and until the groups are created.

The isGroupHiddenWithId() returns the value of the IGroup object’s hidden property. This is used to toggle the display of an accordion group (or uib-accordion-group element).

Create accordion-search-term Directive

We’re almost there. We have our accordion working and we wired up the groups. Now, we need to wire up each .list-group-item by creating a new accordion-search-term directive. This directive will create new ISearchTerm objects using an addSearchTerm() method in the controller. This is very similar to our accordion-group directive.

To get started, create a new file named accordion.search.term.directive.ts and then define a new class called AccordionSearchTerm:

import { AccordionController } from "./accordion.controller";

export class AccordionSearchTermDirective {
 
  public require: string = "^accordion";
  public restrict: string = "AE";
 
  public link = (scope, element, attributes, accordionController: AccordionController) => {
    let groupId: string = attributes.groupId;
    let id: string = attributes.id;
    accordionController.addSearchTerm(element, groupId, id);
  };
 
  public static Factory() {
    return () => new AccordionSearchTermDirective();
  }

}

Some notes:

  • Like the AccordionGroupDirective, we require the accordion directive.
  • In the link() method, we invoke the addSearchTerm() method with the element (JQuery object), the groupId attribute value, and the id attribute value.

Let’s add this to our Accordion module. Here is the completed Accordion module:

import { AccordionController } from "./accordion.controller";
import { AccordionDirective } from "./accordion.directive";
import { AccordionGroupDirective } from "./accordion.group.directive";
import { AccordionSearchTermDirective } from "./accordion.search.term.directive";

export class Accordion {
 
  public static MODULE_NAME = "accordion";
 
  private app;
 
  constructor() {
    this.create().controllers().directives();
  }
 
  public controllers() {
    this.app.controller("AccordionController", AccordionController);
    return this;
  }
 
  public create(): Accordion {
    this.app = angular.module(Accordion.MODULE_NAME, ["ngAnimate", "ui.bootstrap"]);
    return this;
  }
 
  public directives(): Accordion {
    this.app.directive("accordion", AccordionDirective.Factory());
    this.app.directive("accordionGroup", AccordionGroupDirective.Factory());
    this.app.directive("accordionSearchTerm", AccordionSearchTermDirective.Factory());
    return this;
  }
}

All I had to do for the accordion-search-term directive is add the single line to the directives() method.

Now, let’s add some more methods to our controller to keep track of the search terms:

public addSearchTerm(element: Element, groupId: string, id: string): AccordionController {
  //create searchTerm
  let searchTerm: ISearchTerm = {
    element: element,
    groupId: groupId,
    hidden: false,
    id: id
  };

  //store it
  this.searchTerms.push(searchTerm);

  return this;
}

public getSearchTermById(id: string): ISearchTerm {
  for (let searchTerm of this.searchTerms) {
    if (searchTerm.id.toString() === id.toString()) {
      return searchTerm;
    }
  }

  //create an empty searchTerm stub
  let searchTerm: ISearchTerm = {
    groupId: null,
    hidden: false,
    id: null,
    text: null
  };

  return searchTerm;
}

public isItemHiddenWithId(id: string): boolean {
  return this.getSearchTermById(id).hidden;
}

private getTextForSearchTerm(searchTerm: ISearchTerm): string {
  //get the element's text
  let text: string = searchTerm.element.text();

  //verify text
  if (text.trim().length === 0) {
    return;
  }
 
  //clean up text
  text = text.replace(/\W/i, "");
  text = text.trim();
 
  return text;
}

Let’s go through each of these new methods briefly:

  • addSearchTerm() – is invoked by our directive’s link() function. We create the ISearchTerm object and push it onto our this.searchTerms array.
  • getSearchTermById() – returns the ISearchTerm object with the id value specified.
  • isItemHiddenWithId() – we will use this to toggle the display of the accordion-search-term element using the ng-hide directive.
  • getTextForSearchTerm() – this is a private method that will return the innerText of the accordion-search-term element. We do some cleanup of the innerText and then return the string.

Now, we need to modify our .list-group-item as follows:

<a 
  id="{{categoryGroup.id}}_{{$index}}"
  href=""
  class="list-group-item"
  ng-repeat="category in categoryGroup.categories"
  data-group-id="{{categoryGroup.id}}"
  ng-hide="$ctrl.isItemHiddenWithId(categoryGroup.id + '_' + $index)"
  accordion-search-term
>
  {{category}} Classes
</a>

Here is a rundown on the attributes:

  • id – we assign a unique id to the element.
  • href – this can be left blank, but must be specified.
  • class – the Bootstrap list-group-item class.
  • ng-repeat – iterating over the array of string values in the categories property.
  • data-group-id – this matches the id attribute value for the uib-accordion-group.
  • ng-hide – toggle the display of the search term.
  • accordion-search-term – the directive.

Let’s search()

If you have made it this far then you know that we have created three directives and one controller:

  • AccordionDirective: “accordion”
  • AccordionGroupDirective: “accordion-group”
  • AccordionSearchTermDirective: “accordion-search-term”
  • AccordionController: instantiated as “$ctrl”

Now we are ready to implement some more methods in our AccordionController. The first one is the search() method. If you recall, this is invoked from the set q() ES6 mutator for the q property.

The strategy behind our searching is going to be:

  1. Reset any previously stored results.
  2. Do a regex search of the ISearchTerm object element’s innerText.
  3. If no results are found, reset back to showing all accordion groups and items. We will also trigger the display of our .alert element that we added to our template.
  4. If we do have results, let’s start by hiding all of the groups.
  5. Then, show the groups that contain a result.
  6. Then, hide all of the items.
  7. Then, show the items that are a result.

Here is what that looks like:

public search(): AccordionController {
  //log
  console.log(`[AccordionController:search] searching: {q: ${this._q}}.`);

  //reset results
  this.results = [];

  //verify q
  if (this._q.trim().length === 0) {
    return this.clear();
  }

  //do search
  for (let searchTerm of this.searchTerms) {
    let text: string = this.getTextForSearchTerm(searchTerm);
    let matcher: RegExp = new RegExp(this._q, "i");
    let result: RegExpMatchArray = text.match(matcher);
    if (result !== null && result.length > 0) {
      this.results.push(searchTerm);
    }
  }

  //log
  console.log(`[WebucatorAccordionController:search] ${this.results.length} matches.`);

  //verify results exist
  if (this.results.length === 0) {
    this.collapseAll();
    this.showAllGroups();
    this.showAllItems();
    return this;
  }

  //hide all groups
  this.hideAllGroups();

  //show and expand groups in results
  for (let result of this.results) {
    for (let group of this.groups) {
      if (group.id === result.groupId) {
        group.hidden = false;
        group.open = true;
      }
    }
  }

  //hide all items
  this.hideAllItems();

  //show items in results
  for (let result of this.results) {
    result.hidden = false;
  }

  return this;
}

We have created some private methods to make the search() code shorter:

  1. hideAllGroups()
  2. hideAllItems()
  3. showAllGroups()
  4. showAllItems()

These methods simply toggle the hidden property values of the group or item:

private hideAllGroups(): AccordionController {
  //log
  console.log("[AccordionController:hideAllGroups] Hiding all groups.");

  //set each group.hidden property to true
  for (let group of this.groups) {
    group.hidden = true;
  }

  return this;
}
 
private hideAllItems(): AccordionController {
  //log
  console.log("[AccordionController:hideAllItems] Hiding all the list items.");

  //set each searchTerm hidden property to true
  for (let searchTerm of this.searchTerms) {
    searchTerm.hidden = true;
  }

  return this;
}
 
private showAllGroups(): AccordionController {
  //log
  console.log("[AccordionController:showAllGroups] Showing all groups.");

  //set each group.hidden property to false
  for (let group of this.groups) {
    group.hidden = false;
  }

  return this;
}
 
private showAllItems(): AccordionController {
  //log
  console.log("[AccordionController:showAllItems] Showing all items.");

  //set each searchTerm hidden property to false
    for (let searchTerm of this.searchTerms) {
      searchTerm.hidden = false;
    }

  return this;
}

Next, we need to create the clear() method. We added $ctrl.clear() as the ng-click directive expression when we first created the search input in the template. Here is the publicly available search() method:

public clear(): AccordionController {
  this._q = "";
  this.showAllGroups();
  this.collapseAll();
  this.showAllItems();
  this.results = [];
  return this;
}

Some notes on the clear() method:

  • First, we reset the user query string, stored in this._q.
  • Then, we show all of the uib-accordion-group elements.
  • Then, we collapse all of the uib-accordion-group elements.
  • Then, we show all of the .list-group-item elements.
  • Then, we reset the results array.

Next, we need to create the expandAll() and collapse() methods:

public collapseAll(): AccordionController {
  //log
  console.log("[AccordionController:collapseAll] Collapsing all panel groups.");

  //set each group.open property to false
  for (let group of this.groups) {
    group.open = false;
  }

  return this;
}

public expandAll(): AccordionController {
  //log
  console.log("[AccordionController:expandAll] Expanding all panel groups.");

  //set each group.open property to true
  for (let group of this.groups) {
    group.open = true;
  }

  return this;
}

The collapseAll() and expandAll() methods simply toggle the open property value of each IGroup object.

Demo

Check out the working demo on plunker: https://plnkr.co/edit/Nzilmc97H3PpsBDvJQqa?p=preview

About Webucator

Webucator provides instructor-led training to students throughout the US and Canada. We have trained over 90,000 students from over 16,000 organizations on technologies such as Microsoft ASP.NET, Microsoft Office, Azure, Windows, Java, Adobe, Python, SQL, JavaScript, Angular and much more. Check out our complete course catalog.