8
\$\begingroup\$

I am working on a project in Angular where I have a number of similar data objects. When you click on anyone of them it's state and amount of data shown will change. All of the objects start in the minimal state, when you click on one it becomes medium. From a medium you can click on the button to go to maximum or click anywhere else on it to go back down. There can only be one medium or maximum at a time. If there is one medium there can be no maximum, and vice versa.

enter image description here enter image description here

Since all of the objects are by default min, I don't track that. I use a string to track which element is in an expanded state. Then I compare that objects key and the string in an ng-if to change its what is being shown. The size is managed by a conditional class, and CSS.

I chose ng-if, because there maybe a lot of elements loaded in the beginning, but there can only be one med/max at a time. I would rather load the med/max when the user wants it, instead of loading all of the med/max at that start and use ng-show for state view.

Is there a cleaner way to handle state change among many related objects? Does Angular have something to deal with this?

angular.module('SizeAndState', [])
.factory('Data', function() {
        return {
            query: function() {
                return {
                    a:{ name: 'A', date: '1/2/03', number: 1},
                    b:{ name: 'B', date: '4/5/06', number: 2},
                    c:{ name: 'C', date: '8/9/10', number: 3},
                    d:{ name: 'D', date: '3/2/01', number: 4},
                    e:{ name: 'E', date: '1/2/14', number: 5},
                }
            }
        };
    })
    .controller('SizeAndStateController', ['$scope', 'Data', function($scope, Data) {
        $scope.toMin = function (clickEvent){
            clickEvent.stopPropagation();
            $scope.medElement = '';
            $scope.maxElement = '';

        };
        $scope.toMed = function (elementName, clickEvent){
            if($scope.maxElement=='') {
               if($scope.medElement == elementName){
                   $scope.toMin(clickEvent);
               } else {
                   $scope.medElement = elementName;
               }
            }
        };
        $scope.toMax = function (clickEvent){
            clickEvent.stopPropagation();
            $scope.maxElement = $scope.medElement;
            $scope.medElement = '';

        };
        $scope.data = Data.query();
        $scope.medElement = '';
        $scope.maxElement = '';
    }]);
.campaign{
  display: inline-block;
  width: 100px;
  height: 100px;
  background-color: #ff685b;
  border: 1px solid #ffffff;
  color: #ffffff;
  vertical-align: top;
}
.campaign.med{
  width: 125px;
  height: 125px;
}
.campaign.max{
  width: 150px;
  height: 150px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="SizeAndState">
<div ng-controller="SizeAndStateController">
    <div class="campaign"
         ng-repeat = "element in data"
         ng-class="{'med':medElement==element.name, 'max':maxElement==element.name}"
         ng-click="toMed(element.name, $event)">
      
        <span>{{element.name}}</span> <span ng-if='medElement!=element.name && maxElement!=element.name'>Min</span>
		<span ng-if='medElement==element.name'>
			<span>Med</span>
			<h5>{{element.date}}</h5>
            <button class="btn"
                    ng-click="toMax($event)">
					to Max
            </button>
        </span>
      	
      	<span ng-if='maxElement==element.name'>
			<span>Max</span> 
			<h5>{{element.date}}</h5>
			<h5 ng-if='maxElement==element.name'>{{element.number}}</h5>
          	<button
                ng-if="maxElement == element.name"
                ng-click="toMin($event)">
    	        to Min
	        </button>
        </span>      
      
    </div>
</div>
</div>

\$\endgroup\$
  • \$\begingroup\$ Look at $watch it gives you way to track model changes, so if you attach model to you elements you could track changes of elements states. \$\endgroup\$ – outoftime Sep 29 '14 at 23:48
4
+50
\$\begingroup\$

I'd handle the state in pretty much different way. First of all, I'd change the format of data to be an array of objects with unique ids. So instead of

{
  a: { name: 'A', date: '1/2/03', number: 1},
  b: { name: 'B', date: '4/5/06', number: 2},
}

I'd prefer something like below, using state enums as suggested by @Ami.

[{ 
  id: 0, 
  name: 'Category 1',
  items: [{
    id: 0,
    name: 'A1',
    state: state.MIN
  },{
    id: 1,
    name: 'B1',
    state: state.MIN
  },...

With unique ids you can easily and reliably track and modify your objects.

To me, your template's logic (or "flow") is quite hard to follow. So next I would try to reduce the clutter as much as possible.

<body ng-controller="MainCtrl">
    <h3>Size & state</h3>

    <div ng-repeat="collection in data">
        <h4 ng-bind="collection.name"></h4>
        <span ng-repeat="item in collection.items">
            <button ng-bind="item.name"
                    ng-class="{ med: item.state, max: item.state == 2 }"
                    ng-click="changeState(collection, item)">
            </button>
          </span>
    </div>
</body>

Where CSS

.med {
  width: 85px;
  height: 85px;
}

.max {
  width: 110px;
  height: 110px;
}

With these modifications, in controller, changeState(collection, item) function could handle all min/med/max related logic.

switch(item.state) {
  case state.MIN:
    _.each(collection.items, function(current) {
      current.id == item.id ? current.state = state.MED : current.state = state.MIN;
    });
    break;
  case state.MED:
    item.state = state.MAX;
    break;
  default:
    item.state = state.MIN;
}

If selected item's state is MIN, then change it to MED and all other items in given collection will be reset to MIN. If item's state is MED, then just change it to MAX and so on.

Notice the use of _.each(), which is a underscore.js library function. It's a great lib when you need to handle arrays or collections. You didn't want to traverse the collection every time user makes a selection but I don't see it as a performance problem or anything and with uderscore, you can do it pretty much (or very close to) a single-liner.

Here's a related plunker.

enter image description here

\$\endgroup\$
  • \$\begingroup\$ Interesting, but what happens when it's 40 boxes instead of 4? Wouldn't that _.each start to become a problem? \$\endgroup\$ – EasilyBaffled Oct 2 '14 at 12:07
  • \$\begingroup\$ Think all stuff you run on your browser. YouTube, flash games, maybe data intensive business apps. I don't think few simple objects becomes a problem. Edit my plunker and copy/paste a few hundred to begin with. \$\endgroup\$ – Mikko Viitala Oct 2 '14 at 13:44
  • \$\begingroup\$ Actually I forked it for you. Here you have 3x1000 buttons running on plunker, using angular & underscore. On my not-really-highend-machine I see no problems at all plnkr.co/edit/gGA7tI Hope it helps. \$\endgroup\$ – Mikko Viitala Oct 5 '14 at 18:18
  • \$\begingroup\$ While at it, here's a fork doing what you wanted, not iterating but changing only current/prev clicked items plnkr.co/edit/H4bRqS \$\endgroup\$ – Mikko Viitala Oct 5 '14 at 19:00
4
\$\begingroup\$

Your View looks fine (HTML and CSS). However, representing the state of your data elements with two string variables that may or may not be storing the "name" field on the parent data object is a very bad way of representing state.

The guiding principal for these design questions: your object model should make it impossible to ever represent an inconsistent or meaningless state. With your object model there are many ways to represent inconsistent and meaningless states, for example:

  1. It's possible for a data item to be simultaneously in state Med and state Max
  2. If medElement and maxElement variables contain values which are not equal to the name field on the parent object, those values are ignored

As a result your controller code is needlessly complex and very difficult to read.

Solution:

Represent the state of your data objects with an enum called ElementSize that can take exactly one of three possible state values: minimum, medium, and maximum. Then, use the ng-switch directive to specify what html you want to render for each of the three possible states.

\$\endgroup\$
  • \$\begingroup\$ The issue with that would be that I would need an ElementSize for every box, and there could be a lot of them. Since only one element can be medium or maximum at a time, I would have to go through the list to check for an existing med/max each time I wanted to change the size. \$\endgroup\$ – EasilyBaffled Sep 30 '14 at 12:53
  • \$\begingroup\$ @EasilyBaffled The ng-switch could evaluate a function which could keep track of the state. \$\endgroup\$ – konijn Sep 30 '14 at 19:42
1
\$\begingroup\$

@Mikko has a great solution. I tried to add this as a comment to his answer, but it was uggo.

I would change his min block to:

 case state.MIN:
    _.each(collection.items, function(item) {
      item.state = state.MIN;
    });
    selectedItem.state = state.MED;
    break;

Note: I changed the method parameter to selectedItem and the iteration variable to item. It makes more sense to me, but isn't necessary.

This change saves the comparison for every iteration which speeds it up ever so slightly.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.