angular.js directives for interactive web applications

23
AngularJS for Interactive Application Behavior Brent Goldstein [email protected] Practical Directives

Upload: brent-goldstein

Post on 29-Nov-2014

519 views

Category:

Software


2 download

DESCRIPTION

How to build an interactive hierarchical data-grid using custom directives. Shows how to capture keyboard input, navigate the DOM tree with jqLite and display google spreadsheet like selection rectangles.

TRANSCRIPT

Page 1: Angular.js Directives for Interactive Web Applications

AngularJS

for Interactive Application Behavior

Brent Goldstein

[email protected]

Practical Directives

Page 2: Angular.js Directives for Interactive Web Applications

Back Story

● Building an app and want rich behavior○ Don’t limit goals because “it’s a web app”

● Need an interactive Spreadsheet-like Grid○ Hierarchical data, specialized data entry needs○ Similarities to Google Sheets, some differences

● Yikes, this seemed daunting at first○ Hierarchical Data Binding○ Navigation○ Complex element/range selection

Page 3: Angular.js Directives for Interactive Web Applications

Requirements - (be a product mgr)

● Hierarchical Data Seto 3 Row Levels - Person, Projects, Taskso Columns are calendar weeks

● Navigate and Select like a Spreadsheeto Move w/ arrows, tab, shift-tab; mouse-clicko Select single cell or range of cells

● Specialized Editingo Business logic applied to keystrokes before display

Page 4: Angular.js Directives for Interactive Web Applications

How to Render Complex Data?

● Resist thinking like an ‘engineer’

● Visual representation must be intuitive

● Even more important that ‘being’ intuitive→ will someone actually use it?

● Really must pass the dog food test

Page 5: Angular.js Directives for Interactive Web Applications

Digression - Dogfooding?

● OK, this is one of many overused tech industry terms

● Seems to put a rather unappetizing spin on one of the MOST IMPORTANT practices in application design

● It does not matter what you think or say, or what others think or say about using your app

● The application must draw you in (yes, YOU)

● Otherwise, it’s just neat tech, and neat is not a product

Page 6: Angular.js Directives for Interactive Web Applications

Grid Hierarchy Rendering

● Go “Flat” -- flat visual data modeling that is

● Basically, de-normalize where it makes sense→ use grouping to organize

● Greatly simplifies UI construction and interpretation

● Still possible to “drill down”, but using filters and selection to show more/fewer rows

● Avoid “nested grids”; hard to render, hard to read

Page 7: Angular.js Directives for Interactive Web Applications

So how to build it?

- Custom Directive “grid-editor” provides special behaviors- Grid-Editor looks for child DOM elements with special classes

div div div div div div…..

div

…..

…..

<div grid-editor>

<div>

…..

<div class=’grid-person’>

<div class=’grid-person’>

<div>

<div>

Note, grid constructed from divs, not tables, to support custom layout

Page 8: Angular.js Directives for Interactive Web Applications

DOM Location and NavigationLet’s consider one use case, Find the Next cell to the RIGHT

<div grid-editor>

<div>

<div class=’grid-person’>

<div>

...

<div class=’grid-project’>

<div>

<div class=’grid-project’>

<div>

1) At last cell, but not last in row

2) At last cell in row

3) At last cell/row in grid-project

Actually requires handling several sub-cases...

Page 9: Angular.js Directives for Interactive Web Applications

Getting down to Business -- jqLite

<div grid-editor>

<div class=’grid-person’>

col=’1’ col=’2’ col=’3’ col=’5’ ...

<div class=’grid-project’>

col=’4’

1) At last cell, but not last in row

2) At last cell in row

Assuming we know the selected cell, we can find the next like this:

Case 1) Look for next sibling element, use attribute to be selectivevar newElement = currEle.next(‘[col]’);

Case 2) Look for the FIRST element in the NEXT row (requires some maneuvering)var newElement = currEle.closest(‘grid-project’).next(‘grid-project’).find(‘.first’)

Similar ‘traverse up, look for next’ can be applied to higher levels in hierarchy

col=’6’

applied to every cell div: class=’cell’applied to first cell div: class=’first’

Page 10: Angular.js Directives for Interactive Web Applications

What about other cases, like the down arrow key?

Need the next row, but column needs to remain the same. Now that’s why the “col=n” attribute is useful….

Look for next sibling row, use attribute to find the correct columnvar newElement = currEle.closest(‘grid-project’).next(‘grid-project’).find(‘.cell[col=’ + currEle.attr(‘col’) + ‘]’);

Going down, same column, new row

<div grid-editor>

<div class=’grid-person’>

col=’1’ col=’2’ col=’3’ col=’5’ col=’6’

<div class=’grid-project’>

col=’4’

col=’4’

Page 11: Angular.js Directives for Interactive Web Applications

How to capture the arrow keys?

element.bind('keydown keypress', function (e){ if(!scope.editActive) return; // track the editing state

var navArrow = function(e){var arrowKeys = { 37:'left', 38:'up', 39:'right', 40:'down' }; return arrowKeys[e.which]; }; var navTab = function(e){ // tab keyreturn (e.which === 9) ? (e.shiftKey?"left":"right") : null;};

var doNav = navFromArrow(e) || navFromTab(e);if(doNav){

e.preventDefault(); // prevent the browser default actiontheFunctionThatNavigatesToNextCell(doNav)

}};

Can be useful to step outside of Angular for event handling

The ‘element’ above could be the $document root, or a sub-document, depending on the desired behavior. If a sub-doc, the keys will only be captured when that element or children have focus

Page 12: Angular.js Directives for Interactive Web Applications

Rendering the Selection Band

To indicate selected cell, a “rectangle select band” is typically used to surround the cells

How to accomplish this in DOM?

A couple of options:1) Identify and alter the display the underlying DIVs2) Add new elements that Overlay the native grid

Page 13: Angular.js Directives for Interactive Web Applications

Draw band using actual grid cells?

Altering the display of underlying cells can be tricky.

How to actually make the highlight band?- Alter the div/span border(s)

Not ideal. Positioning is limited to element border

Also harder to make a multi-cell rectangle

Page 14: Angular.js Directives for Interactive Web Applications

Overlay Rendering

What about creating a new element/set of elements that display ON TOP of the grid?

Let’s timewarp back to 1998 when graphics were rendered into framebuffers.

Remember the Overlay Framebuffer?

Let’s apply the same logical concept

Page 15: Angular.js Directives for Interactive Web Applications

The Magic Select Rectangle Band

How to render the select rectangle in DOM?

We’ll construct with separate banding elements for max control

It will simplify hover/select options since we can know exactly which side of the band is under the mouse

With this approach we can also add other visual elements, like a cursor select target for dragging, etc.

Top DIV

Bottom DIV

Left

Div

Rig

ht D

iv

Page 16: Angular.js Directives for Interactive Web Applications

Dynamic Element CreationThe select band is managed by the GridEditor Directive:- Create: When the directive is instantiated- Move: When the arrow/tab keys are captured- Grow/Shrink: Based on shift-click

This function is called inside the directive link() function to instantiate the band elements:

var _installBand = function(){ var html = "<div id='magic-square'>"+ "<div class='band-horiz top'></div>" + "<div class='band-horiz bottom'></div>"+ "<div class='band-vert left'></div>"+ "<div class='band-vert right'></div>" + "<div class='band-grabbox'></div>" + "</div>"; _band = angular.element(html); // this creates the dom element _gridEditorElement.prepend(_band); // attach to directive element return _band; // to adjust properties and location later };

Page 17: Angular.js Directives for Interactive Web Applications

Dynamically alter DOM propertiesProperties are updated to adjust the rectangle band size when needed

Note, _unitWidth is already set with the pixel width of a single cell

var _setBandWidth = function(){var rangeWidth = _range(); // gets the desired #cells to cover

// update the top/bottom horizontal div widths_band.children(".band-horiz").width((_unitWidth-1) * rangeWidth);

// update the right vertical div positionvar rightVert = _magicSquare.children(".band-vert.right"); var posLeft = (_unitWidth-1) * rangeWidth -1; rightVert.css({left: posLeft});};

Page 18: Angular.js Directives for Interactive Web Applications

Let’s take a look - Initial View

Top-level view, summary dataUser sees this view when page is loaded

● Threshold coloring (red means too much)● Click on row to expand● Expand All/Collapse All buttons

Page 19: Angular.js Directives for Interactive Web Applications

Let’s take a look - Expanded View

Details for each person visible when expanded

● Project-task rows at same indent level● Grouping by project● Optional project sub-total rows

Page 20: Angular.js Directives for Interactive Web Applications

Let’s take a look - Editing

Behaviors to streamline complex editing needs

● Single Select and Range select● Smart Navigation across projects/people● Shortcut keys for pre-defined operations

Page 21: Angular.js Directives for Interactive Web Applications

Coding tidbit - $emit for messaging

What’s the difference? Well, direction:$emit goes UP, $broadcast goes DOWN

As a result, $emit is more efficient bubbling only to parents$broadcast fans out to all child scopes

Maybe you really need $broadcast... but often not

$emit can be applied in a message-bus pattern to achieve efficient dedicated communication across an app

Both $scope.$broadcast() and $scope.$emit() can be used to communicate between directives and controllers

Page 22: Angular.js Directives for Interactive Web Applications

$emit for messaging

Originating Controller or Directive

Higher level controller or directive

$scope.$emit(‘hello.world’);

$scope.$on(‘hello.world’,function(event){//dosomething));

1) Bubble Up 2) Message Bus

Originating Controller or Directive

RootScope actiing as message Bus

$rootScope.$emit(‘hello.world’);

Listening Directive or Controller

$rootScope.$on(‘hello.world’,function(event){//dosomething));

Message bus approach can often replace $broadcast

Page 23: Angular.js Directives for Interactive Web Applications

contact: [email protected]

thanks!