Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I have been working for weeks on a "BookViewer" AngularJS directive. I would like to write some blog posts on the lessons I learned in writing the directive. Before doing this I would like to ask for some feedback on how I wrote the directive. I ran into a lot trouble, learned a lot, but in the end I am quite happy with the result. I used AngularJS+Typescript+Angular-UI Bootstrap. This is my first attempt to implement a large directive.

The code can be found on GitHub. A live version can be found here.

The visual result of using the directive is as follows:

Sample chapter Sample index

It is possible to switch between a text and index view by setting the indexmode attribute on the directive which I control from a button in the navigation bar, external from the directive.

I made some decisions where I don't know if these are the best decisions. For example:

Using Typescript in the directive

I could not get the directive working with a Typescript class for the controller and the link function. So I use anonymous functions, and use Typescript only for interface definitions and type-checking. In my link function I need to process the DOM, I do this in an anonymous function in the timeout otherwise the DOM is not ready yet. In the anonymous function I need to call other functions. I could not find another way than defining this function on the scope in the controller function. Is it possible to use functions and variables in an instance of a Typescript class?

directive.link = ($scope: IBookViewerScope, $element, $attrs) : void => {
    $timeout(() => {
        $scope.processDom();
        :
    }, 0);
};

The controller function creates a lot of variables and functions in the scope, this was the only way I found to have access to the variables and functions in $timeout callbacks like above. Is this the best approach?

angular.module('directive.bookviewer', [])
    .constant('bookviewerConfig', {
    })
    .controller('bookviewerController', 
       ['$scope', '$element', '$attrs', '$sce', '$location', '$anchorScroll', '$timeout', '$log',
       function($scope:IBookViewerScope, $element, $attrs, $sce, \
                $location:ng.ILocationService, $anchorScroll, $timeout, $log) : void {
           $scope.savedElement = $element; // keep track of element for usage in later code
           $scope.savedAttrs = $attrs;  // keep track of attributes for usage in later code

           $scope.trustAsHtml = (html:string) : any => {
               return $sce.trustAsHtml(html);
           };


           $scope.indexOpenChapterIndex = (chapterIndexId:string) => { ... }
           :
       }]);

Handling fixed bar at top of page

I use a fixed bar at the top. To compensate for this in jumping around in the book I added anchor tags , and in my directive link function I put on each "anchor" element the following code:

$element.css({
    display: "block",
    position: "relative",
    top: offset
});

where offset is -heightOfFixedBar. Problem is that the top of the chapter was still at the wrong offset, so I added a div with the same height as the fixed bar at the top:

<div style="margin: 51px 0 0 0;"/>
    <bookviewer booktoc="vm.toc" chaptercontent="vm.chapter" 
                indexmode="vm.indexmode" offset-element-id="'topnavbar'"
                anchorid="vm.anchorid" on-navigate="vm.navigate(chapterid, anchorid)" 
                on-select="vm.select(text)"/>
</div>

All navigation is done by watching

Initially I started with explicit navigation. I had callback functions connected to links in the html and in those functions I did the scrolling to anchors or callbacks to the function to load the contents of another chapter. I got into a lot of trouble with this, because I also had to respond to changes in the bound parameters of the directive. I ended up with a set of watch functions defined on the scope:

// toggle between text/index view
$scope.$watch('indexmode', (newValue, oldValue) => { ... } 

// respond to click on links
$scope.$watch(() => { return $location.hash(); }, () => { ... } 

// respond to content changes (new chapter content) and anchor changes
$scope.$watch('[chaptercontent.Id, anchorid]', function (newValues, oldValues) { ...}

Although I am intending to use only a single instance of the bookviewer per page, it is a bit of a pity to depend on $location.hash() within a directive. Problem is: I could not find another way while keeping the next/prev button of the browser working.

Rendering with dynamic templates

I do all the rendering with templates defined in the directive.templateUrl.

  • {{paragraph.Title}}
  • <script id="otherChapterIndexTmpl.html" type="text/ng-template">
        <li><a ng-click="navigate(paragraph.Id)" href="javascript:void(0)">{{paragraph.Title}}</a></li>
    </script>
    
    <script id="paragraphTmpl.html" type="text/ng-template">
        <a class="anchor" id="{{paragraph.Id}}"></a>
        <h4>{{paragraph.Title}}</h4>
        <div class="paragraph-text" ng-bind-html="trustAsHtml(paragraph.Content)"></div>
        <ng-include ng-repeat="paragraph in paragraph.Paragraphs" src="'paragraphTmpl.html'"></ng-include>
    </script>
    
    <div class="bookchapter" ng-hide="indexmode">
        <a class="anchor" id="{{chaptercontent.Id}}"></a>
        <h3>{{chaptercontent.Title}}</h3>
        <div class="chapter-text" ng-bind-html="trustAsHtml(chaptercontent.Content)"></div>
        <ng-include ng-repeat="paragraph in chaptercontent.Paragraphs" src="'paragraphTmpl.html'"></ng-include>
    </div>
    
    <div id="bookindex" ng-show="indexmode">
        <h1>Book index</h1>
    
        <accordion close-others="true">
            <accordion-group ng-repeat="tocChapter in booktoc.Chapters" is-open="indexOpenChapterIndex('i-' + tocChapter.Id)">
                <a class="anchor" id="i-{{tocChapter.Id}}"></a>
                <accordion-heading>
                    <a ng-click="navigate(tocChapter.Id)" href="javascript:void(0)">{{tocChapter.Title}}</a>
                    <i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': isopen, 'glyphicon-chevron-right': !isopen}"></i>
                </accordion-heading>
                <ul ng-if="tocChapter.Id === $parent.chaptercontent.Id">
                    <ng-include ng-repeat="paragraph in $parent.chaptercontent.Paragraphs" src="'curChapterIndexTmpl.html'"></ng-include>
                </ul>
                <ul ng-if="tocChapter.Id !== $parent.chaptercontent.Id">
                    <ng-include ng-repeat="paragraph in tocChapter.Paragraphs" src="'otherChapterIndexTmpl.html'"></ng-include>
                </ul>
            </accordion-group>
        </accordion>
    </div>
    

    I must say: I learned my lessons there. I initialy worked with self-closing divs (). This worked when working with jQuery. When I removed jQuery and depended on the AngularJS JQLite implementation, a lot went wrong. So NEVER use self-closing divs. It isn't even supported in the html5 spec. See this.

    I tried to get the code working in Plunker (see this). The solution was to create a "gh-pages" branch of the git repository, so the example is now visible here.

    I hope that this is the appropriate place for asking for feedback, and that I will not be flamed by doing so.

    share|improve this question
    add comment

    Your Answer

     
    discard

    By posting your answer, you agree to the privacy policy and terms of service.

    Browse other questions tagged or ask your own question.