Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Matthew Abbott and Andrew Abbott

Creating A Pixel-Based Version Of ngStyle In AngularJS

By Ben Nadel on

Most of the time, when I used the ngStyle directive in AngularJS, I'm using it to set some kind of offset property (ex, height, width, left, right, bottom, top, ex.). This isn't always the case; but, I'd hazzard a guess that these kinds of CSS properties account for 80% of my ngStyle usage. Furthermore, when I set these values, I'm almost always using "px" as my unit of measurement. As such, I wanted to see if I could create a version of ngStyle that automatically added the "px" unit.


 
 
 

 
  
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When I use ngStyle to translate the View-Model into CSS properties, my HTML markup tends to have an ngStyle attribute that looks like this:

ng-style="{ left: ( thing.x + 'px' ), top: ( thing.y + 'px' ) }"

Having to append the "px" string as part of the object properties adds a lot of noise. The "px" is an implementation detail that clouds the intent of my code, which is to define the CSS properties, "left" and "top", using the properties "x" and "y", respectively. It would make the code easier to write and to read if the "px" was implied.

Of course, I am not suggesting that ngStyle be changed. Rather, what I'd like to do is create a new directive - bnPxStyle - which acts like ngStyle with the caveat that "px" is automatically append to each CSS property value.

I've never created a directive that allowed for inline object definitions before; so, I figured the best thing to do would be to look at how ngStyle works under the hood. It turns out it's quite easy - it's just a (deep) $watch() that's defined the same way you would $watch any $scope value in a directive.

Using ngStyle as a base, I created bnPxStyle. And, to see it work, I created a little demo that allows me to move around a token by clicking on the page. As I click, the event coordinates (pageX and pageY) are used to update the CSS properties (left and top) of the token.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating A Pixel-Based Version Of ngStyle In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Creating A Pixel-Based Version Of ngStyle In AngularJS
  • </h1>
  •  
  • <div bn-board class="board">
  •  
  • <!--
  • Notice that the bn-px-style object properties do NOT include the "px"
  • unit of measurement - these are implied by the directive.
  • -->
  • <div
  • class="token"
  • bn-px-style="{ left: coordinates.x, top: coordinates.y }">
  •  
  • :)
  •  
  • </div>
  •  
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.16.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I define the location of the board token.
  • $scope.coordinates = {
  • x: 50,
  • y: 50
  • };
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I update the positional coordiantes of the board token.
  • $scope.setCoordiantes = function setCoordiantes( newX, newY ) {
  •  
  • $scope.coordinates.x = newX;
  • $scope.coordinates.y = newY;
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I allow the Style attribute of the element to be defined using a dynamic hash.
  • // Each CSS value defined in the given hash will have "px" automatically appended
  • // to the percipitated CSS property.
  • app.directive(
  • "bnPxStyle",
  • function() {
  •  
  • // I bind the JavaScript events to the scope.
  • function link( $scope, element, attributes ) {
  •  
  • // Watch the px-style attribute.
  • $scope.$watch(
  • attributes.bnPxStyle,
  • function watchBnPxStyle( newValue, oldValue ) {
  •  
  • // When the hash of styles changes, we need to remove any
  • // CSS properties that are no longer relevant.
  • if ( newValue && ( newValue !== oldValue ) ) {
  •  
  • clearOldStyles( newValue, oldValue );
  •  
  • }
  •  
  • setNewStyles( newValue );
  •  
  • },
  •  
  • // NOTE: You have to do a DEEP watch here, otherwise, AngularJS
  • // will fall into an infinite digest cycle (I assume because the
  • // reference to the object keeps changing as it is eval'd).
  • true
  • );
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I clear the styles that are no longer present in the new styles
  • // collection.
  • function clearOldStyles( newStyles, oldStyles ) {
  •  
  • for ( var key in oldStyles ) {
  •  
  • // Make sure the property is missing in the new sytles.
  • if ( oldStyles.hasOwnProperty( key ) && ! newStyles.hasOwnProperty( key ) ) {
  •  
  • element.css( key, "" );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  •  
  • // I set the new styles on the element. Each CSS property value is
  • // automatically appended with a "px" unit of measurement.
  • function setNewStyles( newStyles ) {
  •  
  • for ( var key in newStyles ) {
  •  
  • if ( newStyles.hasOwnProperty( key ) ) {
  •  
  • element.css( key, ( newStyles[ key ] + "px" ) );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I update the coordinates view-model based on the location of the click on
  • // the board element.
  • app.directive(
  • "bnBoard",
  • function() {
  •  
  • // I bind the JavaScript events to the scope.
  • function link( $scope, element, attributes ) {
  •  
  • // When the user clicks on the board, update the token coordinates
  • // to match the [localized] coordinates of the event.
  • element.click(
  • function handleClick( event ) {
  •  
  • var globalX = event.pageX;
  • var globalY = event.pageY;
  •  
  • var boardPosition = element.position();
  •  
  • // Translate the coordinates to local element.
  • var localX = ( globalX - boardPosition.left - 2 );
  • var localY = ( globalY - boardPosition.top - 2 );
  •  
  • $scope.$apply(
  • function scopeApply() {
  •  
  • $scope.setCoordiantes( localX, localY );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

This directive is not mutually exclusive with ngStyle. There's no reason that you can't use them at the same time, on the same element. And, for me, this would just add a little bit more readability for the majority of cases in which I use ngStyle.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.