diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts
index d370aab9d6d..6b2a300551e 100644
--- a/public/app/core/components/info_popover.ts
+++ b/public/app/core/components/info_popover.ts
@@ -44,8 +44,9 @@ export function infoPopover() {
}
});
- scope.$on('$destroy', function() {
+ var unbind = scope.$on('$destroy', function() {
drop.destroy();
+ unbind();
});
});
diff --git a/public/app/core/core.ts b/public/app/core/core.ts
index d44cbf4dbfb..5d30bc6d061 100644
--- a/public/app/core/core.ts
+++ b/public/app/core/core.ts
@@ -42,6 +42,8 @@ import './filters/filters';
import coreModule from './core_module';
import appEvents from './app_events';
import colors from './utils/colors';
+import {assignModelProperties} from './utils/model_utils';
+import {contextSrv} from './services/context_srv';
export {
@@ -62,4 +64,6 @@ export {
queryPartEditorDirective,
WizardFlow,
colors,
+ assignModelProperties,
+ contextSrv,
};
diff --git a/public/app/core/utils/emitter.ts b/public/app/core/utils/emitter.ts
index 4cdc19e7b20..2b3f8a90eb4 100644
--- a/public/app/core/utils/emitter.ts
+++ b/public/app/core/utils/emitter.ts
@@ -23,12 +23,17 @@ export class Emitter {
this.emitter.on(name, handler);
if (scope) {
- scope.$on('$destroy', () => {
+ var unbind = scope.$on('$destroy', () => {
this.emitter.off(name, handler);
+ unbind();
});
}
}
+ removeAllListeners(evt?) {
+ this.emitter.removeAllListeners(evt);
+ }
+
off(name, handler) {
this.emitter.off(name, handler);
}
diff --git a/public/app/core/utils/model_utils.ts b/public/app/core/utils/model_utils.ts
new file mode 100644
index 00000000000..3040094b1e3
--- /dev/null
+++ b/public/app/core/utils/model_utils.ts
@@ -0,0 +1,10 @@
+export function assignModelProperties(target, source, defaults) {
+ for (var key in defaults) {
+ if (!defaults.hasOwnProperty(key)) {
+ continue;
+ }
+
+ target[key] = source[key] === undefined ? defaults[key] : source[key];
+ }
+}
+
diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts
index 61c4d658ed1..cd72f6c6922 100644
--- a/public/app/features/alerting/alert_tab_ctrl.ts
+++ b/public/app/features/alerting/alert_tab_ctrl.ts
@@ -52,11 +52,12 @@ export class AlertTabCtrl {
var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
- // set panel alert edit mode
- this.$scope.$on("$destroy", () => {
+ // set panel alert edit mode
+ var unbind = this.$scope.$on("$destroy", () => {
this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
this.panelCtrl.editingThresholds = false;
this.panelCtrl.render();
+ unbind();
});
// build notification model
diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts
index e273a8a3b69..016bb87df3a 100644
--- a/public/app/features/dashboard/dashboard_ctrl.ts
+++ b/public/app/features/dashboard/dashboard_ctrl.ts
@@ -102,12 +102,7 @@ export class DashboardCtrl {
};
$scope.addRowDefault = function() {
- $scope.dashboard.rows.push({
- title: 'New row',
- panels: [],
- height: '250px',
- isNew: true,
- });
+ $scope.dashboard.addEmptyRow();
};
$scope.showJsonEditor = function(evt, options) {
@@ -122,8 +117,9 @@ export class DashboardCtrl {
$timeout.cancel(resizeEventTimeout);
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
});
- $scope.$on('$destroy', function() {
+ var unbind = $scope.$on('$destroy', function() {
angular.element(window).unbind('resize');
+ unbind();
});
};
diff --git a/public/app/features/dashboard/keybindings.js b/public/app/features/dashboard/keybindings.js
index 864538e19c1..d889126c764 100644
--- a/public/app/features/dashboard/keybindings.js
+++ b/public/app/features/dashboard/keybindings.js
@@ -11,8 +11,9 @@ function(angular, $) {
this.shortcuts = function(scope) {
- scope.$on('$destroy', function() {
+ var unbindDestroy = scope.$on('$destroy', function() {
keyboardManager.unbindAll();
+ unbindDestroy();
});
var helpModalScope = null;
@@ -28,7 +29,11 @@ function(angular, $) {
keyboard: false
});
- helpModalScope.$on('$destroy', function() { helpModalScope = null; });
+ var unbindModalDestroy = helpModalScope.$on('$destroy', function() {
+ helpModalScope = null;
+ unbindModalDestroy();
+ });
+
$q.when(helpModal).then(function(modalEl) { modalEl.modal('show'); });
}, { inputDisabled: true });
diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts
index bc67af627f7..aa75800817f 100644
--- a/public/app/features/dashboard/model.ts
+++ b/public/app/features/dashboard/model.ts
@@ -6,8 +6,8 @@ import moment from 'moment';
import _ from 'lodash';
import $ from 'jquery';
-import {Emitter} from 'app/core/core';
-import {contextSrv} from 'app/core/services/context_srv';
+import {Emitter, contextSrv} from 'app/core/core';
+import {DashboardRow} from './row/row_model';
export class DashboardModel {
id: any;
@@ -19,7 +19,7 @@ export class DashboardModel {
timezone: any;
editable: any;
sharedCrosshair: any;
- rows: any;
+ rows: DashboardRow[];
time: any;
timepicker: any;
templating: any;
@@ -51,7 +51,6 @@ export class DashboardModel {
this.timezone = data.timezone || '';
this.editable = data.editable !== false;
this.sharedCrosshair = data.sharedCrosshair || false;
- this.rows = data.rows || [];
this.time = data.time || { from: 'now-6h', to: 'now' };
this.timepicker = data.timepicker || {};
this.templating = this.ensureListExist(data.templating);
@@ -63,10 +62,15 @@ export class DashboardModel {
this.links = data.links || [];
this.gnetId = data.gnetId || null;
+ this.rows = [];
+ if (data.rows) {
+ for (let row of data.rows) {
+ this.rows.push(new DashboardRow(row));
+ }
+ }
+
this.updateSchema(data);
this.initMeta(meta);
-
- this.editMode = this.meta.isNew;
}
private initMeta(meta) {
@@ -84,6 +88,7 @@ export class DashboardModel {
}
this.meta = meta;
+ this.editMode = this.meta.isNew;
}
// cleans meta data and other non peristent state
@@ -91,18 +96,27 @@ export class DashboardModel {
// temp remove stuff
var events = this.events;
var meta = this.meta;
+ var rows = this.rows;
delete this.events;
delete this.meta;
+ // prepare save model
+ this.rows = _.map(this.rows, row => row.getSaveModel());
events.emit('prepare-save-model');
+
var copy = $.extend(true, {}, this);
// restore properties
this.events = events;
this.meta = meta;
+ this.rows = rows;
return copy;
}
+ addEmptyRow() {
+ this.rows.push(new DashboardRow({isNew: true}));
+ }
+
private ensureListExist(data) {
if (!data) { data = {}; }
if (!data.list) { data.list = []; }
diff --git a/public/app/features/dashboard/row/row.ts b/public/app/features/dashboard/row/row.ts
index 97fd238c68e..b996c0109c6 100644
--- a/public/app/features/dashboard/row/row.ts
+++ b/public/app/features/dashboard/row/row.ts
@@ -19,7 +19,7 @@ export class DashRowCtrl {
constructor(private $scope, private $rootScope, private $timeout, private uiSegmentSrv, private $q) {
this.row.title = this.row.title || 'Row title';
- if (this.dashboard.meta.isNew) {
+ if (this.row.isNew) {
this.dropView = 1;
delete this.row.isNew;
}
@@ -200,13 +200,19 @@ coreModule.directive('panelDropZone', function($timeout) {
}
if (indrag === true) {
- return showPanel(dropZoneSpan, 'Drop Here');
+ var dropZoneSpan = 12 - scope.ctrl.dashboard.rowSpan(scope.ctrl.row);
+ if (dropZoneSpan > 1) {
+ return showPanel(dropZoneSpan, 'Drop Here');
+ }
}
hidePanel();
}
- scope.$watchGroup(['ctrl.row.panels.length', 'ctrl.dashboard.editMode', 'ctrl.row.span'], updateState);
+ row.events.on('panel-added', updateState);
+ row.events.on('span-changed', updateState);
+
+ //scope.$watchGroup(['ctrl.row.panels.length', 'ctrl.dashboard.editMode', 'ctrl.row.span'], updateState);
scope.$on("ANGULAR_DRAG_START", function() {
indrag = true;
@@ -220,6 +226,7 @@ coreModule.directive('panelDropZone', function($timeout) {
});
scope.$on("ANGULAR_DRAG_END", function() {
+ console.log('drag end');
indrag = false;
updateState();
});
diff --git a/public/app/features/dashboard/row/row_ctrl.ts b/public/app/features/dashboard/row/row_ctrl.ts
new file mode 100644
index 00000000000..2f5b1446a17
--- /dev/null
+++ b/public/app/features/dashboard/row/row_ctrl.ts
@@ -0,0 +1,231 @@
+///
+
+import _ from 'lodash';
+import $ from 'jquery';
+import angular from 'angular';
+
+import config from 'app/core/config';
+import {coreModule} from 'app/core/core';
+
+import './options';
+import './add_panel';
+
+export class DashRowCtrl {
+ dashboard: any;
+ row: any;
+ dropView: number;
+
+ /** @ngInject */
+ constructor(private $scope, private $rootScope, private $timeout, private uiSegmentSrv, private $q) {
+ this.row.title = this.row.title || 'Row title';
+
+ if (this.dashboard.meta.isNew) {
+ this.dropView = 1;
+ delete this.row.isNew;
+ }
+ }
+
+ onDrop(panelId, dropTarget) {
+ var info = this.dashboard.getPanelInfoById(panelId);
+ if (dropTarget) {
+ var dropInfo = this.dashboard.getPanelInfoById(dropTarget.id);
+ dropInfo.row.panels[dropInfo.index] = info.panel;
+ info.row.panels[info.index] = dropTarget;
+ var dragSpan = info.panel.span;
+ info.panel.span = dropTarget.span;
+ dropTarget.span = dragSpan;
+ } else {
+ info.row.panels.splice(info.index, 1);
+ info.panel.span = 12 - this.dashboard.rowSpan(this.row);
+ this.row.panels.push(info.panel);
+ }
+
+ this.$rootScope.$broadcast('render');
+ }
+
+ setHeight(height) {
+ this.row.height = height;
+ this.$scope.$broadcast('render');
+ }
+
+ moveRow(direction) {
+ var rowsList = this.dashboard.rows;
+ var rowIndex = _.indexOf(rowsList, this.row);
+ var newIndex = rowIndex;
+ switch (direction) {
+ case 'up': {
+ newIndex = rowIndex - 1;
+ break;
+ }
+ case 'down': {
+ newIndex = rowIndex + 1;
+ break;
+ }
+ case 'top': {
+ newIndex = 0;
+ break;
+ }
+ case 'bottom': {
+ newIndex = rowsList.length - 1;
+ break;
+ }
+ default: {
+ newIndex = rowIndex;
+ }
+ }
+ if (newIndex >= 0 && newIndex <= (rowsList.length - 1)) {
+ _.move(rowsList, rowIndex, newIndex);
+ }
+ }
+
+ toggleCollapse() {
+ this.dropView = 0;
+ this.row.collapse = !this.row.collapse;
+ }
+
+ showAddPanel() {
+ this.row.collapse = false;
+ this.dropView = this.dropView === 1 ? 0 : 1;
+ }
+
+ showRowOptions() {
+ this.dropView = this.dropView === 2 ? 0 : 2;
+ }
+}
+
+export function rowDirective($rootScope) {
+ return {
+ restrict: 'E',
+ templateUrl: 'public/app/features/dashboard/row/row.html',
+ controller: DashRowCtrl,
+ bindToController: true,
+ controllerAs: 'ctrl',
+ scope: {
+ dashboard: "=",
+ row: "=",
+ },
+ link: function(scope, element) {
+ scope.$watchGroup(['ctrl.row.collapse', 'ctrl.row.height'], function() {
+ element.find('.panels-wrapper').css({minHeight: scope.ctrl.row.collapse ? '5px' : scope.ctrl.row.height});
+ });
+
+ $rootScope.onAppEvent('panel-fullscreen-enter', function(evt, info) {
+ var hasPanel = _.find(scope.ctrl.row.panels, {id: info.panelId});
+ if (!hasPanel) {
+ element.hide();
+ }
+ }, scope);
+
+ $rootScope.onAppEvent('panel-fullscreen-exit', function() {
+ element.show();
+ }, scope);
+ }
+ };
+}
+
+coreModule.directive('dashRow', rowDirective);
+
+
+coreModule.directive('panelWidth', function($rootScope) {
+ return function(scope, element) {
+ var fullscreen = false;
+
+ function updateWidth() {
+ if (!fullscreen) {
+ element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
+ }
+ }
+
+ $rootScope.onAppEvent('panel-fullscreen-enter', function(evt, info) {
+ fullscreen = true;
+
+ if (scope.panel.id !== info.panelId) {
+ element.hide();
+ } else {
+ element[0].style.width = '100%';
+ }
+ }, scope);
+
+ $rootScope.onAppEvent('panel-fullscreen-exit', function(evt, info) {
+ fullscreen = false;
+
+ if (scope.panel.id !== info.panelId) {
+ element.show();
+ }
+
+ updateWidth();
+ }, scope);
+
+ scope.$watch('panel.span', updateWidth);
+
+ if (fullscreen) {
+ element.hide();
+ }
+ };
+});
+
+
+coreModule.directive('panelDropZone', function($timeout) {
+ return function(scope, element) {
+ var row = scope.ctrl.row;
+ var indrag = false;
+ var textEl = element.find('.panel-drop-zone-text');
+
+ function showPanel(span, text) {
+ element.find('.panel-container').css('height', row.height);
+ element[0].style.width = ((span / 1.2) * 10) + '%';
+ textEl.text(text);
+ element.show();
+ }
+
+ function hidePanel() {
+ element.hide();
+ // element.removeClass('panel-drop-zone--empty');
+ }
+
+ function updateState() {
+ if (scope.ctrl.dashboard.editMode) {
+ if (row.panels.length === 0 && indrag === false) {
+ return showPanel(12, 'Empty Space');
+ }
+
+ var dropZoneSpan = 12 - scope.ctrl.dashboard.rowSpan(scope.ctrl.row);
+ if (dropZoneSpan > 1) {
+ if (indrag) {
+ return showPanel(dropZoneSpan, 'Drop Here');
+ } else {
+ return showPanel(dropZoneSpan, 'Empty Space');
+ }
+ }
+ }
+
+ if (indrag === true) {
+ return showPanel(dropZoneSpan, 'Drop Here');
+ }
+
+ hidePanel();
+ }
+
+ scope.row.events.on('panel-added', updateState);
+ scope.row.events.on('span-changed', updateState);
+
+ scope.$watchGroup(['ctrl.row.panels.length', 'ctrl.dashboard.editMode', 'ctrl.row.span'], updateState);
+
+ scope.$on("ANGULAR_DRAG_START", function() {
+ indrag = true;
+ updateState();
+ // $timeout(function() {
+ // var dropZoneSpan = 12 - scope.ctrl.dashboard.rowSpan(scope.ctrl.row);
+ // if (dropZoneSpan > 0) {
+ // showPanel(dropZoneSpan, 'Panel Drop Zone');
+ // }
+ // });
+ });
+
+ scope.$on("ANGULAR_DRAG_END", function() {
+ indrag = false;
+ updateState();
+ });
+ };
+});
+
diff --git a/public/app/features/dashboard/row/row_model.ts b/public/app/features/dashboard/row/row_model.ts
new file mode 100644
index 00000000000..a3e0a99dba3
--- /dev/null
+++ b/public/app/features/dashboard/row/row_model.ts
@@ -0,0 +1,32 @@
+///
+
+import {Emitter, contextSrv} from 'app/core/core';
+import {assignModelProperties} from 'app/core/core';
+
+export class DashboardRow {
+ panels: any;
+ title: any;
+ showTitle: any;
+ titleSize: any;
+ events: Emitter;
+
+ defaults = {
+ title: 'Dashboard Row',
+ panels: [],
+ showTitle: false,
+ titleSize: 'h6',
+ height: 250,
+ isNew: false,
+ };
+
+ constructor(private model) {
+ assignModelProperties(this, model, this.defaults);
+ this.events = new Emitter();
+ }
+
+ getSaveModel() {
+ assignModelProperties(this.model, this, this.defaults);
+ return this.model;
+ }
+}
+
diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js
index e1132492d4c..60b8f24c462 100644
--- a/public/app/features/dashboard/unsavedChangesSrv.js
+++ b/public/app/features/dashboard/unsavedChangesSrv.js
@@ -65,6 +65,7 @@ function(angular, _) {
dash.time = 0;
dash.refresh = 0;
dash.schemaVersion = 0;
+ dash.editMode = false;
// filter row and panels properties that should be ignored
dash.rows = _.filter(dash.rows, function(row) {
diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js
index 0645114ebf2..6a70de5ad63 100644
--- a/public/app/features/dashboard/viewStateSrv.js
+++ b/public/app/features/dashboard/viewStateSrv.js
@@ -199,8 +199,9 @@ function (angular, _, $) {
}
}
- panelScope.$on('$destroy', function() {
+ var unbind = panelScope.$on('$destroy', function() {
self.panelScopes = _.without(self.panelScopes, panelScope);
+ unbind();
});
};
diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts
index 3bcca4f3f72..52104e5411d 100644
--- a/public/app/features/panel/panel_ctrl.ts
+++ b/public/app/features/panel/panel_ctrl.ts
@@ -50,7 +50,12 @@ export class PanelCtrl {
$scope.$on("refresh", () => this.refresh());
$scope.$on("render", () => this.render());
- $scope.$on("$destroy", () => this.events.emit('panel-teardown'));
+
+ var unbindDestroy = $scope.$on("$destroy", () => {
+ this.events.emit('panel-teardown');
+ this.events.removeAllListeners();
+ unbindDestroy();
+ });
}
init() {
diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts
index 30785eadd69..44c012f772d 100644
--- a/public/app/features/panel/panel_directive.ts
+++ b/public/app/features/panel/panel_directive.ts
@@ -100,7 +100,6 @@ module.directive('grafanaPanel', function() {
panelContainer.removeClass('panel-alert-state--' + lastAlertState);
lastAlertState = null;
}
-
});
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
@@ -189,8 +188,9 @@ module.directive('panelResizer', function($rootScope) {
elem.on('mousedown', dragStartHandler);
- scope.$on("$destroy", function() {
+ var unbind = scope.$on("$destroy", function() {
elem.off('mousedown', dragStartHandler);
+ unbind();
});
}
};
diff --git a/public/app/features/templating/variable.ts b/public/app/features/templating/variable.ts
index 3e12b65ec16..0cd0cb9f847 100644
--- a/public/app/features/templating/variable.ts
+++ b/public/app/features/templating/variable.ts
@@ -2,6 +2,7 @@
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
+import {assignModelProperties} from 'app/core/core';
export interface Variable {
setValue(option);
@@ -13,12 +14,9 @@ export interface Variable {
}
export var variableTypes = {};
-
-export function assignModelProperties(target, source, defaults) {
- _.forEach(defaults, function(value, key) {
- target[key] = source[key] === undefined ? value : source[key];
- });
-}
+export {
+ assignModelProperties
+};
export function containsVariable(...args: any[]) {
var variableName = args[args.length-1];
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index 9d7349e7dbc..ed8cd9462aa 100755
--- a/public/app/plugins/panel/graph/graph.ts
+++ b/public/app/plugins/panel/graph/graph.ts
@@ -34,6 +34,16 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
var rootScope = scope.$root;
var panelWidth = 0;
var thresholdManager = new ThresholdManager(ctrl);
+ var plot;
+
+ ctrl.events.on('panel-teardown', () => {
+ thresholdManager = null;
+
+ if (plot) {
+ plot.destroy();
+ plot = null;
+ }
+ });
rootScope.onAppEvent('setCrosshair', function(event, info) {
// do not need to to this if event is from this panel
@@ -42,7 +52,6 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
}
if (dashboard.sharedCrosshair) {
- var plot = elem.data().plot;
if (plot) {
plot.setCrosshair({ x: info.pos.x, y: info.pos.y });
}
@@ -50,10 +59,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
}, scope);
rootScope.onAppEvent('clearCrosshair', function() {
- var plot = elem.data().plot;
- if (plot) {
- plot.clearCrosshair();
- }
+ plot.clearCrosshair();
}, scope);
// Receive render events
@@ -287,7 +293,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
function callPlot(incrementRenderCounter) {
try {
- $.plot(elem, sortedSeries, options);
+ plot = $.plot(elem, sortedSeries, options);
if (ctrl.renderError) {
delete ctrl.error;
delete ctrl.inspector;
@@ -529,9 +535,9 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
return "%H:%M";
}
- new GraphTooltip(elem, dashboard, scope, function() {
- return sortedSeries;
- });
+ // new GraphTooltip(elem, dashboard, scope, function() {
+ // return sortedSeries;
+ // });
elem.bind("plotselected", function (event, ranges) {
scope.$apply(function() {
diff --git a/public/app/plugins/panel/graph/threshold_manager.ts b/public/app/plugins/panel/graph/threshold_manager.ts
index 069f5f38292..5bb0a8b561a 100644
--- a/public/app/plugins/panel/graph/threshold_manager.ts
+++ b/public/app/plugins/panel/graph/threshold_manager.ts
@@ -153,8 +153,8 @@ export class ThresholdManager {
this.renderHandle(1, this.height-30);
}
- this.placeholder.off('mousedown', '.alert-handle');
- this.placeholder.on('mousedown', '.alert-handle', this.initDragging.bind(this));
+ // this.placeholder.off('mousedown', '.alert-handle');
+ // this.placeholder.on('mousedown', '.alert-handle', this.initDragging.bind(this));
this.needsCleanup = true;
}
diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts
index f323bf4290e..1eac74b2539 100644
--- a/public/app/plugins/panel/graph/thresholds_form.ts
+++ b/public/app/plugins/panel/graph/thresholds_form.ts
@@ -17,9 +17,10 @@ export class ThresholdFormCtrl {
this.disabled = true;
}
- $scope.$on("$destroy", () => {
+ var unbindDestroy = $scope.$on("$destroy", () => {
this.panelCtrl.editingThresholds = false;
this.panelCtrl.render();
+ unbindDestroy();
});
this.panelCtrl.editingThresholds = true;
diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts
index d8d174c5c84..35448d6a296 100644
--- a/public/app/plugins/panel/table/module.ts
+++ b/public/app/plugins/panel/table/module.ts
@@ -212,8 +212,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
elem.on('click', '.table-panel-page-link', switchPage);
- scope.$on('$destroy', function() {
+ var unbindDestroy = scope.$on('$destroy', function() {
elem.off('click', '.table-panel-page-link');
+ unbindDestroy();
});
ctrl.events.on('render', function(renderData) {