In the previous part of the series, we bootstrapped our AngularJS application, configured routing for different views, and built services around routes for posts, users, and categories. Using these services, we are now finally able to fetch data from the server to power the front end.
In this part of the series, we will be working towards building a custom AngularJS directive for the post listing feature. In the current part of the series, we will:
- introduce ourselves to AngularJS directives and why we should create one
- plan the directive for the post listing feature and the arguments it will take
- create a custom AngularJS directive for post listing along with its template
So let’s start by introducing ourselves to AngularJS directives and why we need them.
Introducing AngularJS Directives
Directives in AngularJS are a way to modify the behavior of HTML elements and to reuse a repeatable chunk of code. They can be used to modify the structure of an HTML element and its children, and thus they are a perfect way to introduce custom UI widgets.
While analyzing wireframes in the first part of the series, we noted that the post listing feature is being used in three views, namely:
- Post listing
- Author profile
- Category posts listing
So instead of writing separate functionality to list posts on all of these three pages, we can create a custom AngularJS directive that contains business logic to retrieve posts using the services we created in the earlier part of this series. Apart from business logic, this directive will also contain the rendering logic to list posts on certain views. It’s also in this directive that the functionality for post pagination and retrieving posts on certain criteria will be defined.
Hence, creating a custom AngularJS directive for the post listing feature allows us to define the functionality only in one place, and this will make it easier for us in the future to extend or modify this functionality without having to change the code in all three instances where it’s being used.
Having said that, let’s begin coding our custom directive for the post listing feature.
Planning the Custom AngularJS Directive for Post Listing
Before we begin writing any code for building the directive for the post listing feature, let’s analyze the functionality that’s needed in the directive.
At the very basic level, we need a directive that we could use on our views for post listing, author profile, and the category page. This means that we will be creating a custom UI widget (or a DOM marker) that we place in our HTML, and AngularJS will take care of the rest depending upon what options we provide for that particular instance of the directive.
Hence, we will be creating a custom UI widget identified by the following tag:
<post-listing></post-listing>
But we also need this directive to be flexible, i.e. to take arguments as input and act accordingly. Consider the user profile page where we only want posts belonging to that specific user to show up or the category page where posts belonging to that category will be listed. These arguments can be provided in the following two ways:
- In the URL as parameters
- Directly to the directive as an attribute value
Providing arguments in the URL seems native to the API as we are already familiar with doing so. Hence a user could retrieve a set of posts belonging to a specific user in the following way:
http://127.0.0.1:8080/#/posts?author=1
The above functionality can be achieved by using the $routeParams
service provided by AngularJS. This is where we could access parameters provided by the user in the URL. We have already looked into it while registering routes in the previous part of the series.
As for providing arguments directly to the directive as an attribute value, we could use something like the following:
<post-listing post-args="{author=1}"></post-listing>
The post-args
attribute in the above snippet takes arguments for retrieving a specific set of posts, and currently it’s taking the author ID. This attribute can take any number of arguments for retrieving posts as supported by the /wp/v2/posts
route. So if we were to retrieve a set of posts authored by a user having an ID of 1 and belonging to a category of ID 10, we could do something like the following:
<post-listing post-args="{author=1, filter[cat]=10}"></post-listing>
The filter[cat]
parameter in the above code is used to retrieve a set of posts belonging to a certain category.
Pagination is also an essential feature when working with post listing pages. The directive will handle post pagination, and this feature will be driven by the values of the X-WP-Total
and X-WP-TotalPages
headers as returned by the server along with the response body. Hence, the user will be able to navigate back and forth between the previous and next sets of posts.
Having decided the nitty gritty of the custom directive for post listing, we now have a fairly solid foundation to begin writing the code.
Building a Custom Directive for Post Listing
Building a directive for the post listing feature includes two steps:
- Create the business logic for retrieving posts and handling other stuff.
- Create a rendering view for these posts to show up on the page.
The business logic for our custom directive will be handled in the directive declaration. And for rendering data on the DOM, we will create a custom template for listing posts. Let’s start with the directive declaration.
Directive Declaration
Directives in AngularJS can be declared for a module with the following syntax:
/** * Creating a custom directive for posts listing */ quiescentApp.directive( 'postListing', [function() { return { }; }] );
Here we are declaring a directive on our module using the .directive()
method that’s available in the module. The method takes the name of the directive as the first argument, and this name is closely linked with the name of the element’s tag. Since we want our HTML element to be <post-listing></post-listing>
, we provide a camel-case representation of the tag name. You can learn more about this normalization process performed by AngularJS to match directive names in the official documentation.
The notation we are using in the above code for declaring our directive is called safe-style of dependency injection. And in this notation, we provide an array of dependencies as the second argument that will be needed by the directive. Currently, we haven’t defined any dependencies for our custom directive. But since we need the Posts
service for retrieving posts (that we created in the previous part of the series) and the native AngularJS’s $routeParams
and $location
services for accessing URL parameters and the current path, we define them as follows:
/** * Creating a custom directive for posts listing */ quiescentApp.directive( 'postListing', ['$routeParams', '$location', 'Posts', function( $routeParams, $location, Posts ) { return { restrict: 'E', scope: { postArgs: '=' }, link: function( $scope, $elem, $attr ) { } }; }] );
These dependencies are then made available to the function which is defined as the last element of the array. This function returns an object containing directive definition. Currently, we have two properties in the directive definition object, i.e. restrict
and link
.
The restrict
option defines the way we use directive in our code, and there can be four possible values to this option:
-
A
: For using the directive as an attribute on an existing HTML element. -
E
: For using the directive as an element name.
-
C
: For using the directive as a class name.
-
M
: For using the directive as an HTML comment.
The restrict
option can also accept any combination of the above four values.
Since we want our directive to be a new element <post-listing></post-listing>
, we set the restrict option to E
. If we were to define the directive using the attributes on a pre-existing HTML element, then we could have set this option to A
. In that case, we could use <div post-listing></div>
to define the directive in our HTML code.
The second scope
property is used to modify the scope of the directive. By default, the value of the scope
property is false
, meaning that the scope of the directive is the same as its parent’s. When we pass it an object, an isolated scope is created for the directive and any data that needs to be passed to the directive by its parent is passed through HTML attributes. This is what we are doing in our code, and the attribute we are using is post-args
, which gets normalized into postArgs
.
The postArgs
property in the scope
object can accept any of the following three values:
-
=
: Meaning that the value passed into the attribute would be treated as an object. -
@
: Meaning that the value passed into the attribute would be treated as a plain string.
-
&
: Meaning that the value passed into the attribute would be treated as a function.
Since we have chosen to use the =
value, any value that gets passed into the post-args
attribute would be treated as a JSON object, and we could use that object as an argument for retrieving posts.
The third property, link
, is used to define a function that is used to manipulate the DOM and define APIs and functions that are necessary for the directive. This function is where all the logic of the directive is handled.
The link
function accepts arguments for the scope object, the directive’s HTML element, and an object for attributes defined on the directive’s HTML element. Currently, we are passing two arguments $scope
and $elem
for the scope object and the HTML element respectively.
Let’s define some variable on the $scope
property that we will be using to render the post listing feature on the DOM.
/** * Creating a custom directive for posts listing */ quiescentApp.directive( 'postListing', ['$routeParams', '$location', 'Posts', function( $routeParams, $location, Posts ) { return { restrict: 'E', scope: { postArgs: '=' }, link: function( $scope, $elem, $attr ) { // defining variables on the $scope object $scope.posts = []; $scope.postHeaders = {}; $scope.currentPage = $routeParams.page ? Math.abs( $routeParams.page ) : 1; $scope.nextPage = null; $scope.previousPage = null; $scope.routeContext = $location.path(); } }; }] );
Hence we have defined six properties on the $scope
object that we could access in the DOM. These properties are:
-
$posts
: An array for holding post objects that will be returned by the server. -
$postHeaders
: An object for holding the headers that will be returned by the server along with the response body. We will use these for handling navigation.
-
$currentPage
: An integer variable holding the current page number.
-
$previousPage
: A variable holding the previous page number.
-
$nextPage
: A variable holding the next page number.
-
$routeContext
: For accessing the current path using the$location
service.
The postArgs
property that we defined earlier for HTML attributes will already be available on the $scope
object inside the directive.
Now we are ready to make a request to the server using the Posts
service for retrieving posts. But before that, we must take into account the arguments provided by the user as URL parameters as well as the parameters provided in the post-args
attribute. And for that purpose, we will create a function that uses the $routeParams
service to extract URL parameters and merge them with the arguments provided through the post-args
attribute:
/** * Creating a custom directive for posts listing */ quiescentApp.directive( 'postListing', ['$routeParams', '$location', 'Posts', function( $routeParams, $location, Posts ) { return { restrict: 'E', scope: { postArgs: '=' }, link: function( $scope, $elem, $attr ) { // defining variables on the $scope object $scope.posts = []; $scope.postHeaders = {}; $scope.currentPage = $routeParams.page ? Math.abs( $routeParams.page ) : 1; $scope.nextPage = null; $scope.previousPage = null; $scope.routeContext = $location.path(); // preparing query arguments var prepareQueryArgs = function() { var tempParams = $routeParams; delete tempParams.id; return angular.merge( {}, $scope.postArgs, tempParams ); }; } }; }] );
The prepareQueryArgs()
method in the above code uses the angular.merge()
method, which extends the $scope.postArgs
object with the $routeParams
object. But before merging these two objects, it first deletes the id
property from the $routeParams
object using the delete
operator. This is necessary since we will be using this directive on category and user views, and we don’t want the category and user IDs to get falsely interpreted as the post ID.
Having prepared query arguments, we are finally ready to make a call to the server and retrieve posts, and we do so with the Posts.query()
method, which takes two arguments:
- An object containing arguments for making the query.
- A callback function that executes after the query has been completed.
So we will use the prepareQueryArgs()
function for preparing an object for query arguments, and in the callback function, we set the values of certain variables on the $scope
property:
// make the request and query posts Posts.query( prepareQueryArgs(), function( data, headers ) { $scope.posts = data; $scope.postHeaders = headers(); $scope.previousPage = ( ( $scope.currentPage + 1 ) > $scope.postHeaders['x-wp-totalpages'] ) ? null : ( $scope.currentPage + 1 ); $scope.nextPage = ( ( $scope.currentPage - 1 ) > 0 ) ? ( $scope.currentPage - 1 ) : null; });
The callback function gets passed two arguments for the response body and the response headers. These are represented by the data
and headers
arguments respectively.
The headers
argument is a function that returns an object containing response headers by the server.
The remaining code is pretty self-explanatory as we are setting the value of the $scope.posts
array. For setting the values of the $scope.previousPage
and $scope.nextPage
variables, we are using the x-wp-totalpages
property in the postHeaders
object.
And now we are ready to render this data on the front end using a custom template for our directive.
Creating a Custom Template for the Directive
The last thing we need to do in order to make our directive work is to make a separate template for post listing and link it to the directive. For that purpose, we need to modify the directive declaration and include a templateUrl
property like the following:
/** * Creating a custom directive for posts listing */ quiescentApp.directive( 'postListing', ['$routeParams', '$location', 'Posts', function( $routeParams, $location, Posts ) { return { restrict: 'E', scope: { postArgs: '=' }, templateUrl: 'views/directive-post-listing.html', link: function( $scope, $elem, $attr ) { } }; }] );
This templateUrl
property in the above code refers to a file named directive-post-listing.html in the views directory. So create this file in the views folder and paste in the following HTML code:
<!-- post listing starts --> <article class="post-entry"> <h2 class="post-title"><a href="post-single.html">Good design is a lot like clear thinking made visual.</a></h2> <figure class="post-thumbnail"> <img src="img/img-712-348.jpg" alt="Featured Image"> </figure> <p class="post-meta"> By <a href="author.html">Bilal Shahid</a> in <a href="category.html">Quotes</a> </p> <div class="post-content"> <p>Created days forth. Dominion. Subdue very hath spirit us sixth fish creepeth also. First meat one forth above. You'll Fill for. Can't evening one lights won't. Great of make firmament image. Life his beginning blessed lesser meat spirit blessed seas created green great beginning can't doesn't void moving. Subdue evening make spirit lesser greater all living green firmament winged saw tree one divide wherein divided shall dry very lesser saw, earth the. Light their the.</p> </div> </article> <!-- post listing ends --> <!-- pagination links start --> <div class="post-pagination"> <a href="#" class="button">Older Posts</a> <a href="#" class="button">Newer Posts</a> </div> <!-- pagination links end -->
This is very basic HTML code representing a single post entry and post pagination. I’ve copied it from the views/listing.html file. We will use some AngularJS directives, including ng-repeat
, ng-href
, ng-src
, and ng-bind-html
, to display the data that currently resides in the $scope
property of the directive.
Modify the HTML code to the following:
<!-- post listing starts --> <article class="post-entry" ng-repeat="post in posts"> <h2 class="post-title"><a ng-href="#/posts/{{post.slug}}">{{post.title.rendered}}</a></h2> <figure class="post-thumbnail" ng-show="post.quiescent_featured_image"> <img ng-src="{{post.quiescent_featured_image}}" alt="Featured Image"> </figure> <p class="post-meta"> By <a ng-href="#/users/{{post.author}}">{{post.quiescent_author_name}}</a> in <a ng-href="#/categories/{{category.term_id}}" ng-repeat="category in post.quiescent_categories">{{category.name}}{{$last ? '' : ', '}}</a> </p> <div class="post-content" ng-bind-html="post.excerpt.rendered"></div> </article> <!-- post listing ends -->
The above code uses the ng-repeat
directive to iterate through the $scope.posts
array. Any property that is defined on the $scope
object in the directive declaration is available directly in the template. Hence, we refer to the $scope.posts
array directly as posts
in the template.
By using the ng-repeat
directive, we ensure that the article.post-entry
container will be repeated for each post in the posts
array and each post is referred to as post
in the inner loop. This post
object contains data in the JSON format as returned by the server, containing properties like the post title, post ID, post content, and the featured image link, which is an additional field added by the companion plugin.
In the next step, we replace values like the post title, the post link, and the featured image link with properties in the post
object.
For the pagination, replace the previous code with the following:
<!-- pagination links start --> <div class="post-pagination"> <a ng-href="#{{routeContext}}?page={{nextPage}}" class="button" ng-class="{'disabled': !nextPage}">Newer Posts</a> <a ng-href="#{{routeContext}}?page={{previousPage}}" class="button" ng-class="{'disabled': !previousPage}">Older Posts</a> </div> <!-- pagination links end -->
We first access the routeContext
property, which we defined in our directive declaration, and suffix it with the ?page=
parameter and use the values of the nextPage
and previousPage
variables to navigate back and forth between posts. We also check to see if the next page or the previous page link is not null
, else we add a .disabled
class to the button that is provided by Zurb Foundation.
Now that we've finished the directive, it's time to test it. And we do it by placing a <post-listing></post-listing>
tag in our HTML, ideally right above the <footer></footer>
tag. Doing so means that a post listing will appear above the page footer. Don’t worry about the formatting and styles as we will deal with them in the next part of the series.
So that’s pretty much it for creating a custom AngularJS directive for the post listing feature.
What’s Up Next?
In the current part of the series about creating a front end with the WP REST API and AngularJS, we built a custom AngularJS directive for the post listing feature. This directive uses the Posts
service that we created in the earlier part of the series. The directive also takes user input in the form of an HTML attribute and through URL parameters.
In the concluding part of the series, we will begin working on the final piece of our project, i.e. controllers for posts, users, and categories, and their respective templates.
Comments