0

Angular 1.6.6

There is a dashboard with widgets. I need to be able to have multiple widgets of the same type on the dashboard.

Example of widget object, type - clock:

"clock": {
  "title": "World Clock",
  "group": "Tools",
  "name": "clock",
  "description": "Display date and time",
  "templateUrl": "widgets/clock/view.html",
  "sizeX": 1,
  "sizeY": 1,
  "refresh": false,
  "config": {
    "title": "World Clock",
    "timePattern": "HH:mm:ss",
    "datePattern": "YYYY-MM-DD",
    "location": "Europe/Amsterdam",
    "showseconds": false
  },
  "api": {}
}

The HTML template is

<div gridster="gridsterOptions">
    <ul>
        <li gridster-item="widget" ng-repeat="widget in $ctrl.dashboard.widgets track by $index">
            <p>{{$index}}</p>
            <clock-widget ng-if="widget.name==='clock'" widget=widget
              on-delete="$ctrl.deleteWidget(widget)" on-update="$ctrl.updateWidget(widget)">
            </clock-widget>
        </li> <!-- END gridster-item, widget -->
    </ul>
</div> <!-- END gridsterOptions -->

Widget clock-widget is a component.

After adding the same widget twice I see the following error in the browser console.

Uncaught Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!

If I don't use track by $index I have the duplicates error:

Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. 

Also, I tried to add a unique value to each widget id property and track by id like this

<li gridster-item="widget" ng-repeat="widget in $ctrl.dashboard.widgets track by widget.id">

And I got the Duplicates ... error again.

What is wrong here?


Dashboard component controller:

/*global angular*/
import { findIndex, forEach, cloneDeep } from 'lodash';
import widgets from '../data/widgets';

import addWidgetTemplate from '../dialogs/addWidget/addWidget.dialog.template.html';
import addWidgetController from '../dialogs/addWidget/addWidget.dialog.controller';

const injectParams = [
  '$stateParams',
  '$log',
  'storeService',
  'authService',
  'eventbus',
  'EVENTS',
  'SweetAlert',
  '$state',
  'dialogs',
  '$uibModal'
];

const MainDashboardCtrl = function($stateParams, $log, storeService, authService,
  eventbus, EVENTS, SweetAlert, $state, $dialogs, $uibModal) {

  const self = this;

  self.$onInit = function () {
    storeService.get($stateParams.boardID)
      .then(function (_dashboard_) {
        self.dashboard = _dashboard_;

        if (!self.dashboard) {
          self.dashboard = {};
        }
        if (!self.dashboard.widgets) {
          self.dashboard.widgets = [];
        }

        const currentUser = authService.getCurrentLoginUser();
        if (currentUser.permissions && currentUser.permissions.indexOf('admins') > -1) {
          self.dashboardEditDisable = false;
        }

        if (status.uuid && status.uuid != currentUser.uuid) {
          self.dashboardEditDisable = true;
        } else {
          self.dashboardEditDisable = false;
        }
      })
      .catch(function (error) {
        $log.error('[mainDashboardCtrl]', error);
      });
  };

  self.deleteWidget = function (widget) {
    const index = findIndex(self.dashboard.widgets, w => w.name === widget.name);
    self.dashboard.widgets.splice(index, 1);
  };

  self.updateWidget = function (widget) {
    const index = findIndex(self.dashboard.widgets, w => w.name === widget.name);
    self.dashboard.widgets[index] = cloneDeep(widget);
  };

  // add widget
  self.addWidget = function () {
    $uibModal
      .open({
        controllerAs: '$ctrl',
        controller: addWidgetController,
        template: addWidgetTemplate,
        resolve: {
          widgets: function () {
            return widgets;
          }
        }
      })
      .result.then(function (widget) {
        self.dashboard.widgets.push(widget);
      });
  };

};

MainDashboardCtrl.$inject = injectParams;
export default MainDashboardCtrl;

Dashboard template:

<div class="search-results" access="users" access-permission-type="AtLeastOne">
    <div class="header-action-area">
        <h1 class="lath">
            <small class="text-header-sip">
                <i class="fa fa-h-square" aria-hidden="true" style="margin-right:5px;"></i>
                {{$ctrl.dashboard.name}}
            </small> <!-- END text-header-sip -->
            <button ng-disabled="dashboardEditDisable" ng-click="$ctrl.addWidget()" class="btn btn-whiter btn-primary"
                type="submit" ng-mouseover="hoverA=true" ng-mouseout="hoverA=false">
                <i title="Add New Widget" class="glyphicon glyphicon-plus"></i>
            </button>
        </h1> <!-- END lath -->
    </div> <!-- END header-action-area -->
    <div gridster="gridsterOptions">
        <ul>
            <li gridster-item="widget" ng-repeat="widget in $ctrl.dashboard.widgets">
                <clock-widget ng-if="widget.controllerAs=='clock'" widget=widget
                  on-delete="$ctrl.deleteWidget(widget)" on-update="$ctrl.updateWidget(widget)">
                </clock-widget>
            </li> <!-- END gridster-item, widget -->
        </ul>
    </div> <!-- END gridsterOptions -->
</div> <!-- search-results -->

Widget component controller:

import controller from './clock-widget.settings.controller.js';
import template from '../templates/clock-widget.settings.template.html';

import 'angular-clock/dist/angular-clock.css';
import '../style/clock-widget.css';

import timezones from '../data/timezones';

const injectParams = ['$scope', '$timeout', '$uibModal', '$log'];
const ClockWidgetCtrl = function($scope, $timeout, $uibModal, $log) {
  const self = this;

  const initLocation = function (widget) {
    self.timezones = timezones;
    self.gmtOffset = timezones[widget.config.location];
    self.displayLocation = widget.config.location.split('/')[1].toUpperCase();
  };

  self.delete = function () {
    self.onDelete({ widget: self.widget });
  };

  self.update = function (widget) {
    initLocation(widget);
    self.onUpdate({ widget });
  };

  self.openSettings = function () {
    $uibModal
      .open({
        controllerAs: '$ctrl',
        controller,
        template,
        resolve: {
          widget: function () {
            return self.widget;
          },
          timezones: function () {
            return self.timezones;
          }
        }
      })
      .result.then(function (widget) {
        self.update(widget);
      });
  };

  self.$onInit = function () {
    initLocation(self.widget);
  };

};

ClockWidgetCtrl.$inject = injectParams;
export default ClockWidgetCtrl;
srgbnd
  • 5,404
  • 9
  • 44
  • 80

1 Answers1

0

The ngRepeat directive doesn't like the same object duplicated in an array.

In the dashboard component controller MainDashboardCtrl, a reference to the imported widgets object which includes clock object resolved as input for $uibModal modal.

import widgets from '../data/widgets';
...
$uibModal
  .open({
    ...
    resolve: {
      widgets: function () {
        return widgets;
      }
    }
...

A reference to clock widget is returned as a result of the modal. Then it is added into self.dashboard.widgets array which already contains the same reference to the clock object.

  ...
  .result.then(function (widget) {
    self.dashboard.widgets.push(widget);
  });
  ... 

What we need here, is to add a new, unique object added to the self.dashboard.widgets array.

For example, clonning the clock object:

  ...
  .result.then(function (widget) {
    self.dashboard.widgets.push(cloneDeep(widget));
  });
  ... 
srgbnd
  • 5,404
  • 9
  • 44
  • 80