Build your own HTML element with Angular

facebooktwittergoogle_pluslinkedin

This article is part of the bigger series about my Software Engineering internship this summer. To read all the posts, simply visit the category archive.

In a earlier post I mentioned that I need some standardized way to hide buttons on my view until the user hovers over the containing element. Certainly the way I described it in this post is a valid solution, but there are nicer ways to achieve the same goal by writing more elegant and solid code. By implementing them as a directives. In this post I show you this way step by step.

So what is a directive in Angular? There is of course a neat guide on the official page, where you can find all about these feature. Basically you can write your own HTML-Elements and add behaviour to them. There are two different aspects to that: Directives can simply help you to reduce code duplication. You need some kind of HTML structure all over again? Wrap them in a directive and create your own one. This aspect would mean something like this:

module.directive('panel', function () {
    return {
        restrict:'E',
        transclude:true,
        scope:{ title:'@title' },
        template:'<div class="panel">' +
            '<h3>{{title}}</h3>' +
            '<div class="panel-content" ng-transclude></div>' +
            '</div>',
        replace:true
    };
});

directive.js

It’s kept fairly simple, but already contains neat things. First we declare the name of the directive. In this case it’s panel. With the restrict option we define where we can use this directive. There are four possible values and their combinations: EACM standing for Element, Attribute, Class and Comment. Transclude set to true means, that we will use the content of the element within our directive. In this case we will compile the content and render in where the ng-transclude attributes appears. With the scope attribute it gets a bit more complex. When declaring a object hash here, we’re creating a new ‘isolated’ scope, which means it will only be visible inside our directive. We’re binding the property title to the html attribute title of the directive. Finally we’re describing our template and with the replace attribute we’re stating that the whole element should be replaced by the new content.

So this directive hasn’t any behaviour. It simple let’s use use the following code:

<panel title="My Panel">
  Some content here...
</panel

example.html

Which will result in the following HTML code:

<div class="panel">
  <h3>My Panel</h3>
  <div class="panel-content">
    Some content here...
  </div>
</div>

compiled html as shown in the browser

Add some behaviour

Okay, done that, we can now build something more complex. When writing directives I usually start in the html view where I’ll actually use the future directive and write HTML code down, as it would best fit me. This just feels awesome by doing so, as you can think “If I would have created the HTML, I would have created it this way…”. Who wouldn’t like that!

So, starting with the above directive, the aim no is to add some buttons which will only show up, when the panel is hovered. The first approach was the following:

module.directive('panel', function () {
    return {
        restrict:'E',
        transclude:true,
        scope:{ title:'@title', add:'@add', edit:'@edit'},
        template:'<div ng-mouseenter="active = true" ng-mouseleave="active = false">' +
            '<h3>{{title}}<span >' +
            '<a href="{{add}}" ng-show="active && add">Add</a>' +
            '<a href="{{edit}}" ng-show="active && edit">Edit</a></span></h3>' +
            '<div' +
            '</div>',
        replace:true
    };
});

directive.js

<panel title="Personal Data" edit="#/employee/{{employee.id}}/edit">
  <!-- actual content comes here -->
</panel>

page.html

It’s also pretty straightforward. In addition to the first example I added the html attributes ‘add’ and ‘edit’ and just added some behaviour when the buttons are displayed. But you already see some problems arising here:

  • Sometimes I don’t need both buttons. I only need the edit or the add. That’s why I have to use a double expression to show them.
  • What when I need new buttons? Or I have to name them differntly?
  • What if the header of the panel shouldn’t be in a h3 element?

All in all it worked, but I wasn’t really happy.

Linking directives to each other

My current solution also might not be perfect. I’m just starting to discover the possibilities here and I can quite imagine that there is a far better solution to my problem. But that’s how I currently solved it:

I wrote two different directives:

  • The panel directive is just a container which registered when it’s hovered
  • The options directive is a element which contains all the buttons and is only displayed when the containing panel is hovered

The tricky part is to link them to each other:

module.directive('panel', function () {
    return {
        restrict:'A',
        scope:true,
        controller:function ($scope, $element) {
            $element.addClass("panel");
            $scope.active = false;
            $element.hover(function () {
                $scope.active = true;
                $scope.$apply();
            }, function () {
                $scope.active = false;
                $scope.$apply();
            });
        }
    };
});

directive.js – the panel

The panel now is a simple attribute. It simply creates it’s own scope (a parent scope of the current scope used by the app) and registers its status on it. This is done by the scope attribute together by defining a controller. Note that if you only have one panel on your page you wouldn’t really need the scope: true property. It would then save the state on the parent scope, which is the scope used by your controller. But then all the panels would be considered active if any of them gets hovered. And I would also not want to expose the active state to my parent scope.

In the controller I add a css class to it for styling reasons and then register the jquery hover event listener to the element. So I’m falling back to simple jquery methods: Why does that work when I had quite some troubles in the beginning with it and advised against it? Because here is well encapsulated in the directive and jquery and angular don’t get in each others way. Here I have much more control, to when actually call the jquery functions.

The $scope.$apply() calls actually do the real magic here. They update the scope and fire all events necessary to update the second directive:

module.directive('options', function () {
    return {
        restrict:'E',
        require:'^panel',
        link:function (scope, element) {
            element.addClass("options");
            scope.$watch('active', function (newValue) {
                if (newValue) {
                    element.show();
                } else {
                    element.hide();
                }
            });
        }
    }
});

directive.js – the options

So here I’m declaring a new html element called ‘options’ and I’m stating that it needs a controller from another directive. In this case it looks also for controllers in parent elements (that’s the ‘^’). I then define it’s link function which adds all the behaviour. When called it gets the scope from the parent panel as well. As I’m not using the controller method from the parent, the require property isn’t necessary. But this way it throws an error if there is no parent element with the panel element.

Result

So how does this all look in the end? I created a small jsfiddle for that:

You can see how everything is wired up together and even has some css styling how it could look in the end. As a previously mentioned: I can quite imagine that there is a better solution to this problem. If you have one, feel free to poke me to it 😉

Beside of that, if you have any questions or comments, feel free to share them.

One thought on “Build your own HTML element with Angular

Comments are closed.