<!DOCTYPE html>
<html>
<head>
<!-- 1. Load libraries -->
<!-- Polyfill(s) for older browsers -->
<script src="https://npmcdn.com/core-js/client/shim.min.js"></script>
<script src="https://npmcdn.com/reflect-metadata@0.1.3"></script>
<script src="https://npmcdn.com/systemjs@0.19.27/dist/system.src.js"></script>
<script src="https://code.angularjs.org/1.5.8/angular.js"></script>
<!-- 2. Configure SystemJS -->
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
<link rel="stylesheet" href="style.css">
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
</body>
</html>
/* Styles go here */
System.config({
//use typescript for compilation
transpiler: 'ts',
typescriptOptions: {
tsconfig: true
},
meta: {
'typescript': {
"exports": "ts"
}
},
//map tells the System loader where to look for things
map: {
'app': './app',
'ng-metadata': 'https://npmcdn.com/ng-metadata',
'rxjs': 'https://npmcdn.com/rxjs@5.0.0-beta.6',
'ts': 'https://npmcdn.com/plugin-typescript@4.0.10/lib/plugin.js',
'typescript': 'https://npmcdn.com/typescript@1.9.0-dev.20160409/lib/typescript.js',
},
//packages defines our app package
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
},
'ng-metadata': {
defaultExtension: 'js'
},
'rxjs': {
main: 'index.js',
defaultExtension: 'js'
}
}
});
import { bootstrap } from 'ng-metadata/platform-browser-dynamic';
import { AppComponent, NativeModuleName } from './index';
bootstrap( AppComponent, [NativeModuleName] );
import {Directive, Component, Inject, OnInit, OnDestroy, Input} from 'ng-metadata/core';
import angularStats from "./ng-stats";
var ngCreateCount = 0;
var ngDestroyCount = 0;
var elDestroyCount = 0;
@Directive({
selector: '[my-directive]'
})
class MyDirective {
constructor() {
console.log("MyDirective construct");
}
}
@Component({
selector: 'my-component',
directives:[MyDirective],
template: '<div my-directive="$ctrl.someValue">MyComponent with MyDirective and got created {{$ctrl.inputVal}} times</div>'
})
class MyComponent implements OnInit, OnDestroy {
@Input("<")
public inputVal;
public someValue = "test";
constructor(@Inject("$element") private el) {
}
ngOnInit() {
console.log("MyComponent OnInit");
ngCreateCount++;
this.el.on("$destroy", function() {
console.log("MyComponent element destroyed");
elDestroyCount++;
});
}
ngOnDestroy() {
console.log("MyComponent OnDestroy");
ngDestroyCount++;
}
}
@Component({
selector: 'my-app',
directives:[MyComponent],
template: '<div><h1>My BugReport Plnkr <small>for ng-metadata!</small></h1><div ng-if="$ctrl.showComponent"><my-component input-val="$ctrl.ngCreateCount"></my-component></div><button ng-click="$ctrl.toggle()">Toggle</button><div>ngOnInit: {{ $ctrl.ngCreateCount }}</div><div>ngOnDestroy: {{ $ctrl.ngDestroyCount }}</div><div>el.$destroy: {{ $ctrl.elDestroyCount }}</div></div><hr/><my-native-app></my-native-app>'
})
export class AppComponent {
public showComponent:boolean;
public ngCreateCount:number = 0;
public ngDestroyCount:number = 0;
public elDestroyCount:number = 0;
constructor(@Inject("$rootScope") rootScope) {
angularStats({
rootScope: rootScope,
position: "bottomleft"
});
}
public toggle() {
this.showComponent = !this.showComponent;
this.ngCreateCount = ngCreateCount;
this.ngDestroyCount = ngDestroyCount;
this.elDestroyCount = elDestroyCount;
}
}
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}
export { AppComponent } from './app.component';
export {NativeModuleName} from './app.native.component.ts'
var ngCreateCount = 0;
var ngDestroyCount = 0;
var elDestroyCount = 0;
export var NativeModuleName = angular.module("Native", [])
.directive('myNativeDirective', function() {
return {
controller: function myNativeDirectiveCtrl() {
console.log("MyNativeDirective construct");
}
}
})
.component('myNativeComponent', {
template: '<div my-native-directive>MyNativeComponent with MyNativeDirective and got created {{$ctrl.inputVal}} times</div>',
bindings: {
inputVal: "<"
},
controller: ["$element",function myNativeComponentCtrl($element) {
this.$onInit = function () {
console.log("MyNativeComponent OnInit");
ngCreateCount++;
$element.on("$destroy", function() {
console.log("MyNativeComponent element destroyed");
elDestroyCount++;
});
}
this.$onDestroy = function() {
console.log("MyNativeComponent OnDestroy");
ngDestroyCount++;
}
}]
})
.component('myNativeApp', {
template: '<h1>My BugReport Plnkr <small>with native ng!</small></h1><div ng-if="$ctrl.showComponent"><my-native-component input-val="$ctrl.ngCreateCount"></my-native-component></div><button ng-click="$ctrl.toggle()">Toggle</button><div>$onInit: {{ $ctrl.ngCreateCount }}</div><div>$onDestroy: {{ $ctrl.ngDestroyCount }}</div><div>el.$destroy: {{ $ctrl.elDestroyCount }}</div>',
controller: function myNativeAppCtrl() {
this.showComponent = false;
this.ngCreateCount = 0;
this.ngDestroyCount = 0;
this.elDestroyCount = 0;
this.toggle = function() {
this.showComponent = !this.showComponent;
this.ngCreateCount = ngCreateCount;
this.ngDestroyCount = ngDestroyCount;
this.elDestroyCount = elDestroyCount;
}
}
})
.name;
/* istanbul ignore next */
if (!angular.version) {
// we're doing this because some versions
// of angular don't expose itself correctly
angular = window.angular;
}
export default showAngularStats;
var autoloadKey = 'showAngularStats_autoload';
var current = null;
// define the timer function to use based upon whether or not 'performance is available'
var timerNow = window.self.performance && window.self.performance.now
? () => window.self.performance.now()
: () => Date.now();
var lastWatchCountRun = timerNow();
var watchCountTimeout = null;
var lastWatchCount = getWatcherCount() || 0;
var lastDigestLength = 0;
var scopeSelectors = '.ng-scope, .ng-isolate-scope';
var $rootScope;
var digestIsHijacked = false;
var listeners = {
watchCount: {},
digestLength: {}
};
// Hijack $digest to time it and update data on every digest.
function hijackDigest() {
if (digestIsHijacked) {
return;
}
digestIsHijacked = true;
var scopePrototype = Object.getPrototypeOf(getRootScope());
var oldDigest = scopePrototype.$digest;
scopePrototype.$digest = function $digest() {
var start = timerNow();
oldDigest.apply(this, arguments);
var diff = (timerNow() - start);
updateData(getWatcherCount(), diff);
};
}
// used to prevent localstorage error in chrome packaged apps
function isChromeApp() {
return (typeof chrome !== 'undefined' &&
typeof chrome.storage !== 'undefined' &&
typeof chrome.storage.local !== 'undefined');
}
// check for autoload
var autoloadOptions = sessionStorage[autoloadKey] || (!isChromeApp() && localStorage[autoloadKey]);
if (autoloadOptions) {
autoload(JSON.parse(autoloadOptions));
}
function autoload(options) {
if (window.self.angular && getRootScope()) {
showAngularStats(options);
} else {
// wait for angular to load...
setTimeout(function() {
autoload(options);
}, 200);
}
}
function initOptions(opts) {
// Remove autoload if they did not specifically request it
if (opts === false || !opts.autoload) {
sessionStorage.removeItem(autoloadKey);
localStorage.removeItem(autoloadKey);
// do nothing if the argument is false
if (opts === false) {
return;
}
}
opts.position = opts.position || 'top-left';
opts = angular.extend({
htmlId: null,
rootScope: undefined,
digestTimeThreshold: 16,
watchCountThreshold: 2000,
autoload: false,
trackDigest: false,
trackWatches: false,
logDigest: false,
logWatches: false,
styles: {
position: 'fixed',
background: 'black',
borderBottom: '1px solid #666',
borderRight: '1px solid #666',
color: '#666',
fontFamily: 'Courier',
width: 130,
zIndex: 9999,
textAlign: 'right',
top: opts.position.indexOf('top') === -1 ? null : 0,
bottom: opts.position.indexOf('bottom') === -1 ? null : 0,
right: opts.position.indexOf('right') === -1 ? null : 0,
left: opts.position.indexOf('left') === -1 ? null : 0
}
}, opts || {});
// for ionic support
if (opts.rootScope) {
$rootScope = opts.rootScope;
}
return opts;
}
function showAngularStats(opts) {
/* eslint max-statements:[2, 45] */
/* eslint complexity:[2, 18] */
/* eslint consistent-return:0 */
// TODO ^^ fix these things...
opts = opts !== undefined ? opts : {};
var returnData = {
listeners: listeners
};
// delete the previous one
if (current) {
current.$el && current.$el.remove();
current.active = false;
current = null;
}
// Implemented in separate function due to webpack's statement count limit
opts = initOptions(opts);
if(!opts) {
return;
}
hijackDigest();
// setup the state
var state = current = {active: true};
// auto-load on startup
if (opts.autoload) {
if (opts.autoload === 'localStorage') {
localStorage.setItem(autoloadKey, JSON.stringify(opts));
} else if (opts.autoload === 'sessionStorage' || typeof opts.autoload === 'boolean') {
sessionStorage.setItem(autoloadKey, JSON.stringify(opts));
} else {
throw new Error(
'Invalid value for autoload: ' + opts.autoload + ' can only be "localStorage" "sessionStorage" or boolean.'
);
}
}
// general variables
var bodyEl = angular.element(document.body);
var noDigestSteps = 0;
// add the DOM element
var htmlId = opts.htmlId ? (' id="' + opts.htmlId + '"') : '';
state.$el = angular.element('<div' + htmlId +
'><canvas></canvas><div><span></span> | <span></span></div></div>').css(opts.styles);
bodyEl.append(state.$el);
var $watchCount = state.$el.find('span');
var $digestTime = $watchCount.next();
// initialize the canvas
var graphSz = {width: 130, height: 40};
var cvs = state.$el.find('canvas').attr(graphSz)[0];
// add listeners
listeners.digestLength.ngStatsAddToCanvas = function(digestLength) {
addDataToCanvas(null, digestLength);
};
listeners.watchCount.ngStatsAddToCanvas = function(watchCount) {
addDataToCanvas(watchCount);
};
track('digest', listeners.digestLength);
track('watches', listeners.watchCount, true);
log('digest', listeners.digestLength);
log('watches', listeners.watchCount, true);
function track(thingToTrack, listenerCollection, diffOnly) {
var capThingToTrack = thingToTrack.charAt(0).toUpperCase() + thingToTrack.slice(1);
if (opts['track' + capThingToTrack]) {
returnData[thingToTrack] = [];
listenerCollection['track + capThingToTrack'] = function(tracked) {
if (!diffOnly || returnData[thingToTrack][returnData.length - 1] !== tracked) {
returnData[thingToTrack][returnData.length - 1] = tracked;
returnData[thingToTrack].push(tracked);
}
};
}
}
function log(thingToLog, listenerCollection, diffOnly) {
var capThingToLog = thingToLog.charAt(0).toUpperCase() + thingToLog.slice(1);
if (opts['log' + capThingToLog]) {
var last;
listenerCollection['log' + capThingToLog] = function(tracked) {
if (!diffOnly || last !== tracked) {
last = tracked;
var color = colorLog(thingToLog, tracked);
if (color) {
console.log('%c' + thingToLog + ':', color, tracked);
} else {
console.log(thingToLog + ':', tracked);
}
}
};
}
}
function getColor(metric, threshold) {
if (metric > threshold) {
return 'red';
} else if (metric > 0.7 * threshold) {
return 'orange';
}
return 'green';
}
function colorLog(thingToLog, tracked) {
var color;
if (thingToLog === 'digest') {
color = 'color:' + getColor(tracked, opts.digestTimeThreshold);
} else if (thingToLog === 'watches') {
color = 'color:' + getColor(tracked, opts.watchCountThreshold);
}
return color;
}
function addDataToCanvas(watchCount, digestLength) {
var averageDigest = digestLength || lastDigestLength;
var digestColor = getColor(averageDigest, opts.digestTimeThreshold);
lastWatchCount = nullOrUndef(watchCount) ? lastWatchCount : watchCount;
var watchColor = getColor(lastWatchCount, opts.watchCountThreshold);
lastDigestLength = nullOrUndef(digestLength) ? lastDigestLength : digestLength;
$watchCount.text(lastWatchCount).css({color: watchColor});
$digestTime.text(lastDigestLength.toFixed(2)).css({color: digestColor});
if (!digestLength) {
return;
}
// color the sliver if this is the first step
var ctx = cvs.getContext('2d');
if (noDigestSteps > 0) {
noDigestSteps = 0;
ctx.fillStyle = '#333';
ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);
}
// mark the point on the graph
ctx.fillStyle = digestColor;
ctx.fillRect(graphSz.width - 1, Math.max(0, graphSz.height - averageDigest), 2, 2);
}
// Shift the canvas to the left.
function shiftLeft() {
if (state.active) {
setTimeout(shiftLeft, 250);
var ctx = cvs.getContext('2d');
var imageData = ctx.getImageData(1, 0, graphSz.width - 1, graphSz.height);
ctx.putImageData(imageData, 0, 0);
ctx.fillStyle = ((noDigestSteps++) > 2) ? 'black' : '#333';
ctx.fillRect(graphSz.width - 1, 0, 1, graphSz.height);
}
}
// start everything
shiftLeft();
if (!$rootScope.$$phase) {
$rootScope.$digest();
}
return returnData;
}
angular.module('angularStats', []).directive('angularStats', function() {
var index = 1;
return {
scope: {
digestLength: '@',
watchCount: '@',
watchCountRoot: '@',
onDigestLengthUpdate: '&?',
onWatchCountUpdate: '&?'
},
link: function(scope, el, attrs) {
hijackDigest();
var directiveIndex = index++;
setupDigestLengthElement();
setupWatchCountElement();
addWatchCountListener();
addDigestLengthListener();
scope.$on('$destroy', destroyListeners);
function setupDigestLengthElement() {
if (attrs.hasOwnProperty('digestLength')) {
var digestEl = el;
if (attrs.digestLength) {
digestEl = angular.element(el[0].querySelector(attrs.digestLength));
}
listeners.digestLength['ngStatsDirective' + directiveIndex] = function(length) {
window.dirDigestNode = digestEl[0];
digestEl.text((length || 0).toFixed(2));
};
}
}
function setupWatchCountElement() {
if (attrs.hasOwnProperty('watchCount')) {
var watchCountRoot;
var watchCountEl = el;
if (scope.watchCount) {
watchCountEl = angular.element(el[0].querySelector(attrs.watchCount));
}
if (scope.watchCountRoot) {
if (scope.watchCountRoot === 'this') {
watchCountRoot = el;
} else {
// In the case this directive is being compiled and it's not in the dom,
// we're going to do the find from the root of what we have...
var rootParent;
if (attrs.hasOwnProperty('watchCountOfChild')) {
rootParent = el[0];
} else {
rootParent = findRootOfElement(el);
}
watchCountRoot = angular.element(rootParent.querySelector(scope.watchCountRoot));
if (!watchCountRoot.length) {
throw new Error('no element at selector: ' + scope.watchCountRoot);
}
}
}
listeners.watchCount['ngStatsDirective' + directiveIndex] = function(count) {
var watchCount = count;
if (watchCountRoot) {
watchCount = getWatcherCountForElement(watchCountRoot);
}
watchCountEl.text(watchCount);
};
}
}
function addWatchCountListener() {
if (attrs.hasOwnProperty('onWatchCountUpdate')) {
listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex] = function(count) {
scope.onWatchCountUpdate({watchCount: count});
};
}
}
function addDigestLengthListener() {
if (attrs.hasOwnProperty('onDigestLengthUpdate')) {
listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex] = function(length) {
scope.onDigestLengthUpdate({digestLength: length});
};
}
}
function destroyListeners() {
delete listeners.digestLength['ngStatsDirectiveUpdate' + directiveIndex];
delete listeners.watchCount['ngStatsDirectiveUpdate' + directiveIndex];
delete listeners.digestLength['ngStatsDirective' + directiveIndex];
delete listeners.watchCount['ngStatsDirective' + directiveIndex];
}
}
};
function findRootOfElement(el) {
var parent = el[0];
while (parent.parentElement) {
parent = parent.parentElement;
}
return parent;
}
});
// UTILITY FUNCTIONS
function getRootScope() {
if ($rootScope) {
return $rootScope;
}
var scopeEl = document.querySelector(scopeSelectors);
if (!scopeEl) {
return null;
}
$rootScope = angular.element(scopeEl).scope().$root;
return $rootScope;
}
// Uses timeouts to ensure that this is only run every 300ms (it's a perf bottleneck)
function getWatcherCount() {
clearTimeout(watchCountTimeout);
var now = timerNow();
if (now - lastWatchCountRun > 300) {
lastWatchCountRun = now;
lastWatchCount = getWatcherCountForScope();
} else {
watchCountTimeout = setTimeout(function() {
updateData(getWatcherCount());
}, 350);
}
return lastWatchCount;
}
function getWatcherCountForElement(element) {
var startingScope = getClosestChildScope(element);
return getWatcherCountForScope(startingScope);
}
function getClosestChildScope(element) {
element = angular.element(element);
var scope = element.scope();
if (!scope) {
element = angular.element(element.querySelector(scopeSelectors));
scope = element.scope();
}
return scope;
}
function getWatchersFromScope(scope) {
return scope && scope.$$watchers ? scope.$$watchers : [];
}
// iterate through listeners to call them with the watchCount and digestLength
function updateData(watchCount, digestLength) {
// update the listeners
if (!nullOrUndef(watchCount)) {
angular.forEach(listeners.watchCount, function(listener) {
listener(watchCount);
});
}
if (!nullOrUndef(digestLength)) {
angular.forEach(listeners.digestLength, function(listener) {
listener(digestLength);
});
}
}
function nullOrUndef(item) {
return item === null || item === undefined;
}
function getWatcherCountForScope(scope) {
var count = 0;
iterateScopes(scope, function(childScope) {
count += getWatchersFromScope(childScope).length;
});
return count;
}
function iterateScopes(currentScope, fn) {
if (typeof currentScope === 'function') {
fn = currentScope;
currentScope = null;
}
currentScope = currentScope || getRootScope();
currentScope = _makeScopeReference(currentScope);
if (!currentScope) {
return;
}
var ret = fn(currentScope);
if (ret === false) {
return ret;
}
return iterateChildren(currentScope, fn);
}
function iterateSiblings(start, fn) {
var ret;
/* eslint no-extra-boolean-cast:0 */
while (!!(start = start.$$nextSibling)) {
ret = fn(start);
if (ret === false) {
break;
}
ret = iterateChildren(start, fn);
if (ret === false) {
break;
}
}
return ret;
}
function iterateChildren(start, fn) {
var ret;
while (!!(start = start.$$childHead)) {
ret = fn(start);
if (ret === false) {
break;
}
ret = iterateSiblings(start, fn);
if (ret === false) {
break;
}
}
return ret;
}
function getScopeById(id) {
var myScope = null;
iterateScopes(function(scope) {
if (scope.$id === id) {
myScope = scope;
return false;
}
});
return myScope;
}
function _makeScopeReference(scope) {
if (_isScopeId(scope)) {
scope = getScopeById(scope);
}
return scope;
}
function _isScopeId(scope) {
return typeof scope === 'string' || typeof scope === 'number';
}