I decided to take a look back at AngularJS this week after having spent the last few months with other front-end DSL environments and platforms. (EmberJS and, to the extent it qualifies, Jekyll/Liquid).
I think the time I spent learning EmberJS late in 2016 really began to get me in the mind-set of web components. More on EmberJS in other posts, and I’ll leave the “meta” discussion of web components up to the trend setters…I’m following you though!
AngularJS has been actively moving toward components itself, even in Angular 1.X which has an aura of being ‘deprecated’.
As RTFM works, you can find out these things! What’s more, some precursors to components were there in angularJS all along.
What I’m Sharing:
- a basic example of a directive that can modularize “sortable tables”
- a few examples of AngularJS bells and whistles in use
- an example how you can pass different actions to the same reusable custom directive you create.
Really it is that last one, the passing of a function name to a directive, enabling the same directive to be used in different ways, that made me want to share this.
While I’m not sure I nailed the “AngularJS way”, the process had me taking a deeper dive.
The AngularJS website and style guides didn’t really have any great examples so, maybe seeing my syntax will get your juices flowing.
A quick reminder why to use directives
First, open up the (external) live demo to see the two sortable lists where we’ll reuse page segments that are more than flat information displays.
Second, open the code toggle below for something ugly!
<div class="container" >
<div class="col-md-12">
<div ng-controller="RatingListController as list ">
<div class="col-md-3 master-list">
<table id="all-performer-table" class="table table-bordered table-striped">
<thead ng-init="sortType='score'; sortReverse=true ">
<tr>
<td>
<a href="#" ng-click="sortType = '-name'; sortReverse = !sortReverse">
Select From Below
<span ng-show="sortType == '-name' && sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == '-name' && !sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td>
<a href="#" ng-click="sortType = 'score'; sortReverse = !sortReverse">
Score
<span ng-show="sortType == 'score' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'score' && sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td class="check-box-label">
+<span style="color: red;"> -</span>
</td>
</tr>
</thead>
<tr ng-repeat="item in list.itemsInSet | orderBy:sortType:sortReverse">
<td ng-class="{limeHighlight: item.selected}" >{{item.name}}</td>
<td>{{item.score}}</td>
<td><input type="checkbox" ng-model=item.selected ng-click="list.addOrRemoveFromRatedItems(item)"></td>
</tr>
</table>
</div>
<div class="col-md-4 selected-list">
<ol class="selected-item-list">
<li ng-repeat="selItem in list.ratedItems" class="selected-item-rated">
<span >{{selItem.name}}- {{selItem.$$hashKey}}</span>
<div class="list-making-tools pull-right" >
<span class="glyphicon glyphicon-remove-circle text-danger" ng-click="list.removeFromRatedItems(selItem)"></span>
<span class="glyphicon glyphicon-arrow-down text-warning " ng-click="list.moveDownRatedItems(selItem)"></span>
<span class="glyphicon glyphicon-arrow-up text-success" ng-click="list.moveUpRatedItems(selItem)"></span>
</div>
</li>
</ol>
</div>
</div> <!-- end RatingListController -->
<div class="col-md-5 demo-explanation " ng-controller="PhotoDisplayController as photos">
<div class="photo-box">
<img ng-src="{{photos.chosenPhoto.photo_url}}" alt="{{photos.chosenPhoto.title}}">
</div>
<table id="photo-table" class="table table-bordered table-striped">
<thead ng-init="sortType='score'; sortReverse=true ">
<tr>
<td>
<a href="#" ng-click="sortType = '-name'; sortReverse = !sortReverse">
Select From Below
<span ng-show="sortType == '-name' && sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == '-name' && !sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td>
<a href="#" ng-click="sortType = 'score'; sortReverse = !sortReverse">
Year
<span ng-show="sortType == 'score' && !sortReverse" class="fa fa-caret-down"></span>
<span ng-show="sortType == 'score' && sortReverse" class="fa fa-caret-up"></span>
</a>
</td>
<td class="check-box-label">
+<span style="color: red;"> -</span>
</td>
</tr>
</thead>
<tr ng-repeat="item in photos.itemsInSet | orderBy:sortType:sortReverse">
<td ng-class="{limeHighlight: item.selected}" >{{item.name}}</td>
<td>{{item.year}}</td>
<td><input type="checkbox" ng-model=item.selected ng-click="list.addOrRemoveFromRatedItems(item)"></td>
</tr>
</table>
</div>
</div>
</div>
That sort of mess of logic in the view is why AngularJS got a bad rap… but! … that was never how it was supposed to be!
Here is how that HTML page looks with the content broken out into directives:
<div class="col-md-12">
<div class="first-two-columns" ng-controller="RatingListController as list ">
<div class="col-md-3 master-list">
<tn-sortable-table
list-data='list' cols-displayed="name,score" cols-labels="Character,Rating" check-action='addOrRemoveFromRatedItems' sort-col='score' sort-desc='true'>
</tn-sortable-table>
</div>
<div class="col-md-4 selected-list">
<user-selected-list></user-selected-list>
</div>
</div> <!-- end RatingListCotnroller -->
<div class="col-md-5 photo-display-list " ng-controller="PhotoDisplayController as photos">
<div class="photo-box">
<img ng-src="{{photos.chosenPhoto.photo_url}}" alt="{{photos.chosenPhoto.title}}">
</div>
<tn-sortable-table
list-data='photos' cols-displayed="title,category,year" cols-labels="Photos of Tom,Activity,Year" check-action="pickPhoto" sort-col='title' sort-desc='false'>
</tn-sortable-table>
</div> <!-- end PhotoDisplayController -->
</div>
See? The main page segments are called by custom tag with their specific code factored out.
The key element is the creation of the DSL like <sortable-table>
tags.
You can use data from mutiple controllers, name the data columns you want to see, give custom column labels, and specify which columns and direction to sort by.
AND, you can dictate what action will be performed after checking…all from one directive line.
I will indent and comment for easier reading:
<tn-sortable-table
list-data='list' <!--we actually pass all the functions down to the directive to enable action-->
<!--for more than a demo controllers another param would be added here for custom data-->
cols-displayed="name,score"<!--These Two columns of the data table will be displayed in this order-->
cols-labels="Character Name,Rating"<!--Label the headers in a user friendly way-->
sort-col='score' sort-desc='true'<!--On load, show table sorted by it's 'score' column descending-->
check-action='addOrRemoveFromRatedItems'><!--this is the Tricky Part : pass a function name-->
</tn-sortable-table>
The first above lets a visitor choose names from a master list and put them into their own order.
You can see my project where you can use something similar to make Top Ten Lists and have your votes added to the public score as well as make image of your own opinions to share via social media.
And my second use of the directive, this time as a control feature for a photo display as you saw in the linked demo.
<tn-sortable-table
list-data='photos' <!-- same directive, different controller -->
cols-displayed="title,category,year"<!--Three columns this time - any number of cols works-->
cols-labels="Photos of Tom,Activity,Year"<!--user can click on any table header label to resort-->
sort-col='title' sort-desc='false'
check-action="pickPhoto" >
</tn-sortable-table>
The parameters: list-data , passes all the list data downward as well as the scope needed to execute your action.
The ‘cols-displayed’ and ‘cols-labels’ do what you think.. the first is used as a key and the second as text in the table headers. Note, AngularJS will not take arrays. I chose comma separated strings as the easiest alternative to type.
It is easy to allow a content creator to choose what column the tables should be ranked on when landing with sort-col (name of the data key column) and sort-desc (true or false).
The checkAction is a string name of the function on the controller that will be called.
You can see the directive’s JS file below:
Here is the directive’s JS file that handles those the <tn-sortable-table>
tags above.
// js/directives/sortable-table.js
angular
.module('demo-directive-app')
.directive('tnSortableTable', sortableTable);
function sortableTable(){
return {
restrict: 'E',
scope:{
listData : "=listData"
},
templateUrl: 'templates/directives/sortable-table.html',
link: function(scope, element, attrs){
scope.colsDisplayed = attrs.colsDisplayed.split(',');
scope.colsLabels = attrs.colsLabels.split(',');
scope.sortCol = attrs.sortCol;
scope.sortDesc = (attrs.sortDesc == "true");
scope.itemsInSet = scope.listData.itemsInSet;
scope.checkAction = function(item){
scope.listData[attrs.checkAction](item);
}
}
};
};
There are a few tricky things there. The kebab - separated names get ‘normalized’ into camel case in the JS file. (i.e. cols-displayed must be changed to colsDisplayed).
I use the setter format “=listData” to expand the page scope into the directive scope. An alternative way that you need when you’ll manipulate the data is the use of the ‘link: function() ‘ syntax.
I am going to cop-out on a true explanation of how this all works. That would be longer than this post all together.
Here are a few links Stack overflow essay on AngularJS inheritance and the main AngularJS developer guide on directives
I worry somewhat about the elegance of passing the entire scope via ‘listData’ however it’s important to realize …. and you might pound your head against the table for a few hours … that you can’t have ng-click
’s given action build a regex or function via interpolated elements of expressions!
from their site:
No function declarations or RegExp creation with literal notation
You can’t declare functions or create regular expressions from within AngularJS expressions. This is to avoid complex model transformation logic inside templates. Such logic is better placed in a controller or in a dedicated filter where it can be tested properly.
So, that explains why I was sure that the parent scope of the controller was available to the directive via the “listData” param.
You CAN use a directive parameter to -be executed- . Ng-click looks at what is between the “quotes” and first tries to execute it rather than interpolating {{}} first then executing. Many work-arounds attempts will stop the entire page from executing.
What I do below is one working approach:
<!-- templates/sortable-table.html -->
<table id="all-performer-table" class="table table-bordered table-striped">
<thead ng-init="sortType=colTwo; sortReverse=true ">
<tr>
<td sortable-table-header inner-type='sortType' inner-reverse ='sortReverse' this-col='colOne'></td>
<td sortable-table-header inner-type='sortType' inner-reverse ='sortReverse' this-col='colTwo'></td >
<td class="check-box-label">
+<span style="color: red;"> -</span>
</td>
</tr>
</thead>
<tr ng-repeat="item in listData.itemsInSet | orderBy:sortType:sortReverse" >
<td ng-class="{limeHighlight: item.selected}" >
{{item[colOne]}}
</td>
<td>
{{item[colTwo]}}
</td>
<td>
<input type="checkbox" ng-model=item.selected ng-click="listData[checkAction](item)">
</td>
</tr>
</table>
You’ll see that I have yet another directive, the
That inner hidden directive is passed a column name to label the column from the parent parameter.
- the single quotes around ‘colOne’ passes teh value of ‘colOne’, not the string “colOne”.
- to retrieve the column information using that dynamic parameter, use bracket notation.
**By breaking out the header elements further, we cleans out the repeated glyphicon references. Even more importantly it is a step towards being able to iterate over an array of column names to accommodate a variable number of columns using a single directive. **
A developer won’t need to know the internal hand-off as it is encapsulated within the outer tags like a DSL.
For good measure though, here is the internal working:
// js/sortable-2col-header.js
angular
.module('demo-directive-app')
.directive('sortableTableHeader', sortableTableHeader);
function sortableTableHeader(){
return {
scope: {
thisCol : "=thisCol",
innerReverse : "=innerReverse",
innerType : "=innerType"
},
templateUrl: 'templates/sortable-table-header-cell.html'
};
};
<!-- templates/sortable-table-header-cell.html -->
<table id="all-performer-table" class="table table-bordered table-striped">
<thead >
<tr>
<td ng-repeat="label in colsLabels">
<a href="" ng-click="
$parent.sortCol == $parent.colsDisplayed[$index]
? $parent.sortDesc = !$parent.sortDesc
: $parent.sortCol = $parent.colsDisplayed[$index]"
>
{{label}}
<span ng-show="$parent.sortCol == $parent.colsDisplayed[$index] && $parent.sortDesc" class="fa fa-caret-down"></span>
<span ng-show="$parent.sortCol == $parent.colsDisplayed[$index] && !$parent.sortDesc" class="fa fa-caret-up"></span>
</a>
</td >
<td class="check-box-label">
+
<span style="color: red;"> -</span>
</td>
</tr>
</thead>
<tr ng-repeat="item in itemsInSet | orderBy:sortCol:sortDesc" >
<td ng-repeat="col in colsDisplayed" ng-class="{limeHighlight: item.selected}" >
{{$parent.item[col]}}
</td>
<td>
<input type="checkbox" ng-model=item.selected ng-click="$parent.checkAction(item)" >
</td>
</tr>
</table>
I hope these examples might help someone toying with the same issues out.
Note that last line!
<input type="checkbox" ng-model=item.selected ng-click="$parent.checkAction(item)" >
That might have been worth reading this blog entry alone if you’d been in a pinch. (LOL).
I could point out more in the template above, but most of it is basic sorting and filtering mechanisms that make AngularJS so easy to use for state centric displays.
I will -not- share the two controllers code with you. They’re very basic scaffolds to let the demo actually do something when you click.
Again, check out the live demo And/Or you can check out the entire little sandbox in this angularjs-custom-directive-demo GitHub repository.
Within that Repo you can check out a few earlier iterations, AND check out my more extensive and ambitious handlebars solution to the same types of displays but with more robust and reusable controllers and interesting approaches to making the pages maintain display state without watches and two way binding.
Post on that will be linked here soon!.