Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created November 27, 2015 15:45
Show Gist options
  • Save bennadel/fea53b74006267216b57 to your computer and use it in GitHub Desktop.
Save bennadel/fea53b74006267216b57 to your computer and use it in GitHub Desktop.
Sometimes, There Is Unavoidable Coupling To The DOM In AngularJS
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Sometimes, There Is Unavoidable Coupling To The DOM In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Sometimes, There Is Unavoidable Coupling To The DOM In AngularJS
</h1>
<dot-grid>
<ul>
<li
ng-repeat="dot in dg.dots track by dot.id"
ng-style="{ left: dot.x, top: dot.y }">
<span>{{ dot.id }}</span>
</li>
</ul>
</dot-grid>
<!-- 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.4.7.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide a dot-grid component that the user can click to add new dots. The
// dots rearrange to be evenly distributed as the set-count increases.
angular.module( "Demo" ).directive(
"dotGrid",
function dotGridDirective( $window ) {
// Return the directive configuration object.
return({
controller: DotGridController,
controllerAs: "dg",
link: link,
restrict: "E"
});
// I bind the JavaScript events to the view-model.
function link( scope, element, attributes, controller ) {
// When the user clicks on the grid, we have to add a new dot.
element.on( "click", handleClick );
// When the user resizes the window, we have to adjust the layout.
angular.element( $window ).on( "resize", handleResize );
// Report the initial layout to the controller.
scope.$evalAsync( reportLayout );
// ---
// PRIVATE METHODS.
// ---
// When the user clicks on the grid, we need to tell the controller
// to add a new dot at the click coordinates.
function handleClick( event ) {
scope.$apply(
function changeModel() {
var offset = element.offset();
controller.addDot(
( event.pageX - offset.left ),
( event.pageY - offset.top )
);
}
);
}
// When the window is resized, it changes the dimensions of the
// gird. But, since the Controller doesn't know anything about the
// DOM, we have to report the new dimensions.
function handleResize() {
// NOTE: Using applyAsync() for micro-debouncing.
scope.$applyAsync( reportLayout );
}
// I report the current physical dimensions of the grid to the
// controller so that it knows how to distribute the dots.
function reportLayout() {
controller.setDimensions( element.width(), element.height() );
}
}
// I manage the dot-grid.
function DotGridController( $scope, $timeout ) {
var vm = this;
// I hold the collection of rendered dots.
vm.dots = [];
// I keep track of the dimensions and cardinality of the grid.
var grid = {
columns: 0,
rows: 0,
width: 0,
height: 0
};
// Expose the public methods.
vm.addDot = addDot;
vm.setDimensions = setDimensions;
// ---
// PUBLIC METHODS.
// ---
// I add a dot at the given x/y pixel coordinates.
function addDot( x, y ) {
vm.dots.push({
id: ( vm.dots.length + 1 ),
x: x,
y: y
});
// CAUTION: This is the real point of coupling between the DOM
// and the controller. The reason we have to use the $timeout()
// here is because we want the initial x/y coordinates to render
// on the new dot before we adjust the layout to keep the dots
// evenly distributed.
//
// I can't think of a nicer way to do this. We either build the
// coupling into this method by way of the timeout; or, we let
// the link() function coordinate an explicit call to
// adjustLayout(), at which point, the two layers are still
// coupled because the controller has to know NOT to call
// adjustLayout(), even though it would make sense from a data
// point of view, in order to facilitate the animation.
$timeout( adjustLayout, 50 );
}
// I set the new dimensions of the grid.
function setDimensions( width, height ) {
grid.width = width;
grid.height = height;
adjustLayout();
}
// ---
// PRIVATE METHODS.
// ---
// I adjust the layout of the grid - both in column and row count as
// well as in spacing - to accommodate the current number of dots.
function adjustLayout() {
var maxCapacity = ( grid.rows * grid.columns );
// Add columns or rows to be able to fit the dots.
if ( vm.dots.length > maxCapacity ) {
if ( ! grid.columns ) {
grid.columns = grid.rows = 1;
} else if ( grid.columns === grid.rows ) {
grid.columns++;
} else {
grid.rows++;
}
}
// Calculate the inter-column and inter-row spacing needed
// to keep the dots evenly distributed within the grid.
var columnSpacing = Math.floor( grid.width / ( grid.columns + 1 ) );
var rowSpacing = Math.floor( grid.height / ( grid.rows + 1 ) );
// Iterate over each dot and adjust the x/y coordinates to
// keep the dots evenly distributed.
for ( var i = 0, length = vm.dots.length ; i < length ; i++ ) {
var dot = vm.dots[ i ];
var currentColumn = ( i % grid.columns );
var currentRow = Math.floor( i / grid.columns );
dot.x = ( columnSpacing * ( currentColumn + 1 ) );
dot.y = ( rowSpacing * ( currentRow + 1 ) );
}
}
}
}
);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment