Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Justin Hinden
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Justin Hinden

Creating A Pixel-Based Version Of ngStyle In AngularJS

By
Published in Comments (2)

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.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel