├── packages ├── devtools-page │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── containers │ │ │ │ ├── performance │ │ │ │ │ ├── performance.component.css │ │ │ │ │ ├── performance.component.html │ │ │ │ │ ├── performance.component.ts │ │ │ │ │ └── performance.module.ts │ │ │ │ └── component-tree │ │ │ │ │ ├── component-tree.component.css │ │ │ │ │ ├── tree-diagram │ │ │ │ │ ├── tree-diagram.component.scss │ │ │ │ │ ├── tree-diagram.component.html │ │ │ │ │ └── tree-diagram.component.ts │ │ │ │ │ ├── component-info │ │ │ │ │ ├── component-info.component.scss │ │ │ │ │ ├── component-info.component.ts │ │ │ │ │ └── component-info.component.html │ │ │ │ │ ├── component-tree.component.html │ │ │ │ │ ├── component-tree.module.ts │ │ │ │ │ └── component-tree.component.ts │ │ │ ├── app.component.scss │ │ │ ├── shared │ │ │ │ ├── split-pane │ │ │ │ │ ├── split-pane.component.scss │ │ │ │ │ ├── split-pane.component.html │ │ │ │ │ └── split-pane.component.ts │ │ │ │ └── shared.module.ts │ │ │ ├── channel │ │ │ │ └── connection.ts │ │ │ ├── app.component.html │ │ │ ├── app.module.ts │ │ │ ├── core │ │ │ │ └── view.service.ts │ │ │ └── app.component.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── styles.css │ │ ├── main.ts │ │ ├── index.html │ │ ├── test.ts │ │ └── polyfills.ts │ ├── .editorconfig │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── browserslist │ ├── tsconfig.json │ ├── .gitignore │ ├── README.md │ ├── karma.conf.js │ ├── package.json │ ├── tslint.json │ └── angular.json ├── devtools.ts ├── core │ ├── util │ │ ├── zone.ts │ │ ├── profiling.ts │ │ ├── canvas.ts │ │ └── treeView.ts │ ├── package.json │ ├── constants.ts │ ├── angular │ │ ├── interfaces │ │ │ ├── sanitization.ts │ │ │ ├── type_checks.ts │ │ │ ├── context.ts │ │ │ ├── document.ts │ │ │ ├── projection.ts │ │ │ ├── player.ts │ │ │ ├── container.ts │ │ │ ├── renderer.ts │ │ │ ├── definition.ts │ │ │ ├── node.ts │ │ │ └── view.ts │ │ ├── util │ │ │ ├── view_utils.ts │ │ │ └── view_traversal_utils.ts │ │ └── render3 │ │ │ └── context_discovery.ts │ └── index.ts ├── communication │ ├── message.type.ts │ └── messager.ts ├── background.ts ├── popup │ └── popup.ts └── content-script.ts ├── .gitignore ├── images ├── state.png ├── icon128.png ├── icon16.png ├── icon48.png ├── highlight.png ├── ng-profiler.gif ├── component-tree.gif ├── component-tree.png ├── icon16_disabled.png ├── icon48_disabled.png └── icon148_disabled.png ├── cleanup.sh ├── devtools.html ├── package.json ├── tsconfig.json ├── webpack.config.js ├── manifest.json ├── README.md └── popup.html /packages/devtools-page/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.idea 4 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/performance/performance.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/component-tree.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/state.png -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/icon128.png -------------------------------------------------------------------------------- /images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/icon16.png -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/icon48.png -------------------------------------------------------------------------------- /images/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/highlight.png -------------------------------------------------------------------------------- /images/ng-profiler.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/ng-profiler.gif -------------------------------------------------------------------------------- /images/component-tree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/component-tree.gif -------------------------------------------------------------------------------- /images/component-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/component-tree.png -------------------------------------------------------------------------------- /images/icon16_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/icon16_disabled.png -------------------------------------------------------------------------------- /images/icon48_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/icon48_disabled.png -------------------------------------------------------------------------------- /images/icon148_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/images/icon148_disabled.png -------------------------------------------------------------------------------- /packages/devtools-page/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /packages/devtools-page/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-yangy/ng-profiler/HEAD/packages/devtools-page/src/favicon.ico -------------------------------------------------------------------------------- /packages/devtools.ts: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create( 2 | 'Angular Profiler', 3 | '../images/profiler.png', 4 | 'index.html' 5 | ); 6 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | find . -type f -name 'yarn.lock' -exec rm {} + 2 | find . -type f -name 'package-lock.json' -exec rm {} + 3 | find . -name "node_modules" -type d -prune -exec rm -rf '{}' + 4 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/tree-diagram/tree-diagram.component.scss: -------------------------------------------------------------------------------- 1 | nz-button-group { 2 | position: fixed; 3 | i { 4 | font-size: 20px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/util/zone.ts: -------------------------------------------------------------------------------- 1 | declare const Zone; 2 | 3 | export function scheduleOutsideOfZone(scheduledFn: () => void) { 4 | Zone.root.run(() => new Promise(r => r()).then(() => scheduledFn())); 5 | } -------------------------------------------------------------------------------- /packages/devtools-page/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | nz-tabset { 2 | height: 100%; 3 | ::ng-deep { 4 | .ant-tabs-content, 5 | .ant-tabs-tabpane { 6 | height: 100% 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "shortid": "^2.2.15" 6 | }, 7 | "devDependencies": { 8 | "@angular/core": "^9.0.6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/devtools-page/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, body { height: 100%; } 4 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 5 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/component-info/component-info.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | padding: 5px; 3 | } 4 | .form-buttons { 5 | display: flex; 6 | justify-content: flex-end; 7 | button { 8 | margin: 10px; 9 | } 10 | } -------------------------------------------------------------------------------- /packages/core/constants.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = [ 2 | '#bfbfbf', 3 | '#1890ff', 4 | '#52c41a', 5 | '#fadb14', 6 | '#f5222d' 7 | ]; 8 | 9 | export const NG_PROFILER_ID = '__ng_profiler_component_id'; 10 | 11 | export const DRAWER_THRESHOLD = 20; 12 | 13 | export const UPDATE_DEBOUNCE_TIME = 1000; -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/tree-diagram/tree-diagram.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/devtools-page/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /packages/devtools-page/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | "**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/devtools-page/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/shared/split-pane/split-pane.component.scss: -------------------------------------------------------------------------------- 1 | .pane-body { 2 | display: flex; 3 | height: 100%; 4 | .pane-primary { 5 | box-sizing: border-box; 6 | border-right: 1px solid rgba(0,0,0,.24); 7 | overflow: auto; 8 | } 9 | .pane-secondary { 10 | box-sizing: border-box; 11 | border-left: 1px solid rgba(0,0,0,.24); 12 | overflow: auto; 13 | padding-bottom: 100px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/devtools-page/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/sanitization.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** 10 | * Function used to sanitize the value before writing it into the renderer. 11 | */ 12 | export type SanitizerFn = (value: any, tagName?: string, propName?: string) => string; 13 | -------------------------------------------------------------------------------- /packages/devtools-page/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/performance/performance.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Id 5 | Name 6 | OnPush 7 | 8 | 9 | 10 | 11 | {{data.id}} 12 | {{data.name}} 13 | {{data.onPush}} 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/performance/performance.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ViewService } from "@devtools-page/core/view.service"; 3 | 4 | @Component({ 5 | selector: 'performance', 6 | templateUrl: './performance.component.html', 7 | styleUrls: ['./performance.component.css'] 8 | }) 9 | export class PerformanceComponent implements OnInit { 10 | constructor(public viewService: ViewService) { } 11 | 12 | ngOnInit(): void { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/performance/performance.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { PerformanceComponent } from './performance.component'; 4 | import { SharedModule } from "@devtools-page/shared/shared.module"; 5 | 6 | 7 | 8 | @NgModule({ 9 | declarations: [PerformanceComponent], 10 | imports: [ 11 | CommonModule, 12 | SharedModule 13 | ], 14 | exports: [PerformanceComponent] 15 | }) 16 | export class PerformanceModule { } 17 | -------------------------------------------------------------------------------- /packages/devtools-page/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "experimentalDecorators": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "target": "es2015", 13 | "lib": [ 14 | "es2018", 15 | "dom" 16 | ] 17 | }, 18 | "angularCompilerOptions": { 19 | "fullTemplateTypeCheck": true, 20 | "strictInjectionParameters": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/devtools-page/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DevtoolsPage 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/channel/connection.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgZone } from '@angular/core'; 2 | 3 | declare const chrome: any; 4 | 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class Connection { 10 | bgConnection; 11 | 12 | constructor(private zone: NgZone) { 13 | this.connect(); 14 | } 15 | 16 | connect() { 17 | try { 18 | const name = chrome.devtools.inspectedWindow.tabId.toString() || 'tab'; 19 | this.bgConnection = chrome.runtime.connect({ name }); 20 | console.log(name, this.bgConnection); 21 | } catch (e) { 22 | console.log(e); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/component-tree.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Angular Profiler is off

5 |
6 |
7 | 8 | 9 | 10 | No component selected 11 |
12 |
-------------------------------------------------------------------------------- /packages/communication/message.type.ts: -------------------------------------------------------------------------------- 1 | export const MESSAGE_SOURCE = 'NG_PROFILER'; 2 | 3 | export interface Message { 4 | id?: string; 5 | source?: string; 6 | type?: MessageType; 7 | method?: MessageMethod; 8 | content?: T; 9 | } 10 | 11 | export enum MessageMethod { 12 | Request = 'request', 13 | Response = 'response' 14 | } 15 | 16 | export enum MessageType { 17 | TOGGLE_PROFILING = 'TOGGLE_PROFILING', 18 | IS_IVY = 'IS_IVY', 19 | COMPONENT_TREE = 'COMPONENT_TREE', 20 | UPDATE_TREE = 'UPDATE_TREE', 21 | APPLY_CHANGES = 'APPLY_CHANGES', 22 | HIGHLIGHT_ELEMENT = 'HIGHLIGHT_ELEMENT', 23 | HIGHLIGHT_VIEW = 'HIGHLIGHT_VIEW', 24 | HIGHLIGHT_TREE = 'HIGHLIGHT_TREE', 25 | VIEW_PROFILES = 'VIEW_PROFILES' 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-profiler", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "cleanup": "./cleanup.sh", 11 | "build": "webpack", 12 | "watch": "webpack --watch", 13 | "devtool-page:build:watch": "cd packages/devtools-page && yarn build:watch" 14 | }, 15 | "author": "Andrew Yang", 16 | "license": "MIT", 17 | "dependencies": { 18 | "rxjs": "^6.5.4" 19 | }, 20 | "devDependencies": { 21 | "@types/chrome": "0.0.83", 22 | "copy-webpack-plugin": "^5.1.1", 23 | "ts-loader": "^5.4.5", 24 | "typescript": "^3.8.3", 25 | "webpack": "^4.42.0", 26 | "webpack-cli": "^3.3.11" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/devtools-page/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "buildOnSave": false, 4 | "compilerOptions": { 5 | "lib": ["es2015", "dom"], 6 | "target": "es5", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "noImplicitAny": false, 10 | "removeComments": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "outDir": "./dist/", 16 | "paths": { 17 | "@communication/*": ["packages/communication/*"], 18 | "@core/*": ["packages/core/*"], 19 | "@devtools-page/*": ["packages/devtools-page/src/app/*"], 20 | "@popup/*": ["packages/popup/*"] 21 | } 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "dist" 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/component-tree.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ComponentTreeComponent } from "./component-tree.component"; 4 | import { TreeDiagramComponent } from "./tree-diagram/tree-diagram.component"; 5 | import { SharedModule } from "../../shared/shared.module"; 6 | import { ComponentInfoComponent } from "./component-info/component-info.component"; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | ComponentTreeComponent, 11 | TreeDiagramComponent, 12 | ComponentInfoComponent 13 | ], 14 | imports: [ 15 | CommonModule, 16 | SharedModule 17 | ], 18 | exports: [ 19 | ComponentTreeComponent 20 | ] 21 | }) 22 | export class ComponentTreeModule { } 23 | -------------------------------------------------------------------------------- /packages/devtools-page/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /packages/devtools-page/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/shared/split-pane/split-pane.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | 6 |
7 |
8 | 9 |
10 | 11 |
15 |
16 | 17 |
20 |
21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode:'development', 6 | entry: { 7 | 'content-script': './packages/content-script', 8 | core: './packages/core', 9 | background: './packages/background', 10 | devtools: './packages/devtools', 11 | popup: './packages/popup/popup.ts' 12 | }, 13 | devtool: 'source-map', 14 | cache: true, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.ts$/, 19 | use: 'ts-loader', 20 | exclude: /node_modules/ 21 | } 22 | ] 23 | }, 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'] 26 | }, 27 | output: { 28 | filename: '[name].bundle.js', 29 | path: path.resolve(__dirname, 'dist') 30 | }, 31 | plugins: [ 32 | new CopyPlugin([ 33 | { from: 'manifest.json', to: 'manifest.json' }, 34 | { from: 'devtools.html', to: 'devtools.html' }, 35 | { from: 'popup.html', to: 'popup.html' }, 36 | { from: 'images', to: 'images' }, 37 | ]), 38 | ] 39 | }; -------------------------------------------------------------------------------- /packages/core/angular/util/view_utils.ts: -------------------------------------------------------------------------------- 1 | import { LContext, MONKEY_PATCH_KEY_NAME } from "../interfaces/context"; 2 | import { CHILD_HEAD, HOST, LView } from "../interfaces/view"; 3 | import { isLView } from "../interfaces/type_checks"; 4 | 5 | export function readPatchedData(target: any = {}): LView|LContext|null { 6 | return target[MONKEY_PATCH_KEY_NAME] || null; 7 | } 8 | 9 | export function readPatchedLView(target: any): LView|null { 10 | const value = readPatchedData(target); 11 | if (value) { 12 | return Array.isArray(value) ? value : (value as LContext).lView; 13 | } 14 | return null; 15 | } 16 | 17 | export function findAngularVersion(view: LView) { 18 | try { 19 | return (view[CHILD_HEAD][HOST] as any).getAttribute('ng-version'); 20 | } catch (e) {} 21 | } 22 | 23 | export function getComponentLViewByIndex(nodeIndex: number, hostView: LView): LView { 24 | // Could be an LView or an LContainer. If LContainer, unwrap to find LView. 25 | const slotValue = hostView[nodeIndex]; 26 | const lView = isLView(slotValue) ? slotValue : slotValue[HOST]; 27 | return lView; 28 | } -------------------------------------------------------------------------------- /packages/devtools-page/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { SharedModule } from "./shared/shared.module"; 7 | import { NZ_ICONS } from "ng-zorro-antd"; 8 | import { IconDefinition } from '@ant-design/icons-angular'; 9 | import { ZoomInOutline, ZoomOutOutline } from '@ant-design/icons-angular/icons'; 10 | import { ComponentTreeModule } from "./containers/component-tree/component-tree.module"; 11 | import { PerformanceModule } from "./containers/performance/performance.module"; 12 | const icons: IconDefinition[] = [ ZoomInOutline, ZoomOutOutline ]; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | ], 18 | imports: [ 19 | BrowserModule, 20 | BrowserAnimationsModule, 21 | SharedModule, 22 | ComponentTreeModule, 23 | PerformanceModule 24 | ], 25 | providers: [{ provide: NZ_ICONS, useValue: icons }], 26 | bootstrap: [AppComponent] 27 | }) 28 | export class AppModule { } 29 | -------------------------------------------------------------------------------- /packages/devtools-page/README.md: -------------------------------------------------------------------------------- 1 | # DevtoolsPage 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /packages/core/angular/render3/context_discovery.ts: -------------------------------------------------------------------------------- 1 | import { LView } from "../interfaces/view"; 2 | import { readPatchedLView } from "../util/view_utils"; 3 | 4 | export function isComponentInstance(instance: any): boolean { 5 | return instance && instance.constructor && instance.constructor.ɵcmp; 6 | } 7 | 8 | export function isDirectiveInstance(instance: any): boolean { 9 | return instance && instance.constructor && instance.constructor.ɵdir; 10 | } 11 | 12 | export function isProvidereInstance(instance: any): boolean { 13 | return instance && instance.constructor && instance.constructor.ɵprov; 14 | } 15 | 16 | export function findLView(target: HTMLElement | ChildNode | Node): LView|null { 17 | if (!target || !target.childNodes) { 18 | return; 19 | } 20 | const childNodes = target.childNodes; 21 | for (let i = 0; i < childNodes.length; i++) { 22 | const childNode = childNodes[i]; 23 | let mpValue = readPatchedLView(childNode); 24 | if (mpValue) { 25 | return mpValue; 26 | } else { 27 | const mpValueChildren = findLView(childNode); 28 | if (mpValueChildren) { 29 | return mpValueChildren; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /packages/core/util/profiling.ts: -------------------------------------------------------------------------------- 1 | import { getRootView } from "../angular/util/view_traversal_utils"; 2 | import { CONTEXT, RootContext } from "../angular/interfaces/view"; 3 | import { readPatchedLView } from "../angular/util/view_utils"; 4 | import { CanvasFactory } from "./canvas"; 5 | import { SerializedTreeViewItem, TreeViewFactory } from "./treeView"; 6 | 7 | export const startProfiling = () => { 8 | CanvasFactory.create(); 9 | 10 | TreeViewFactory.enable(); 11 | 12 | patchComponentTree(); 13 | }; 14 | 15 | export const stopProfiling = () => { 16 | TreeViewFactory.disable(); 17 | }; 18 | 19 | export const patchComponentTree = (generateSerialisedTreeView?: (serialisedTreeView: SerializedTreeViewItem) => void) => { 20 | const view = TreeViewFactory.bodyLView; 21 | const root = getRootView(view); 22 | const rootComponent = (root[CONTEXT] as RootContext).components[0]; 23 | const rootComponentLView = readPatchedLView(rootComponent); 24 | TreeViewFactory.attachComponent(rootComponentLView, (treeView) => { 25 | TreeViewFactory.setView(treeView); 26 | generateSerialisedTreeView && generateSerialisedTreeView(TreeViewFactory.serialisedTreeView); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/devtools-page/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/devtools-page'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "version": "0.2.4", 4 | "name": "Angular Profiler", 5 | "short_name": "ng profiler", 6 | "description": "Angular Profiler is a developer tool to power-up Angular development workflow by providing a series of debugging tools.", 7 | "content_scripts": [ 8 | { 9 | "matches": [ 10 | "" 11 | ], 12 | "js": ["content-script.bundle.js"] 13 | } 14 | ], 15 | "background": { 16 | "scripts": ["background.bundle.js"] 17 | }, 18 | "devtools_page": "devtools.html", 19 | "permissions": ["activeTab", "storage"], 20 | "browser_action": { 21 | "default_icon": { 22 | "16": "images/icon16_disabled.png", 23 | "48": "images/icon48_disabled.png", 24 | "148": "images/icon148_disabled.png" 25 | }, 26 | "default_title": "Angular Profiler", 27 | "default_popup": "popup.html" 28 | }, 29 | "icons": { 30 | "16": "images/icon16.png", 31 | "48": "images/icon48.png", 32 | "128": "images/icon128.png" 33 | }, 34 | "web_accessible_resources": ["node_modules/*", "dist/*", "*"], 35 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'" 36 | } -------------------------------------------------------------------------------- /packages/communication/messager.ts: -------------------------------------------------------------------------------- 1 | import { fromEvent, Observable, of } from "rxjs"; 2 | import { concatMapTo, filter, pluck, take } from "rxjs/operators"; 3 | import { Message, MESSAGE_SOURCE, MessageMethod, MessageType } from "./message.type"; 4 | 5 | export const createMessage = (type: MessageType, method: MessageMethod, content?: T,): Message => ({ 6 | source: MESSAGE_SOURCE, 7 | method, 8 | type, 9 | content 10 | }); 11 | 12 | export const observeMessage = (message: Message): Observable => { 13 | return of(window.postMessage(message, '*')) 14 | .pipe( 15 | concatMapTo(fromEvent(window, 'message')), 16 | pluck>('data'), 17 | filter(m => m.method === MessageMethod.Response), 18 | filter(m => m.type === message.type), 19 | take(1), 20 | pluck, T>('content') 21 | ) 22 | }; 23 | 24 | export const observeResponse = (type: MessageType): Observable => { 25 | return fromEvent(window, 'message') 26 | .pipe( 27 | pluck>('data'), 28 | filter(m => m.method === MessageMethod.Response), 29 | filter(m => m.type === type), 30 | pluck, T>('content') 31 | ) 32 | }; 33 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/core/view.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from "rxjs"; 3 | import { filter, pluck } from "rxjs/operators"; 4 | import { SerializedTreeViewItem } from "../containers/component-tree/tree-diagram/tree-diagram.component"; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ViewService { 10 | private selectedNode$ = new BehaviorSubject(null); 11 | private componentTreeView$ = new BehaviorSubject(null); 12 | private updatedTreeId$ = new BehaviorSubject(null); 13 | 14 | constructor() { } 15 | 16 | get selectedNode() { 17 | return this.selectedNode$.asObservable(); 18 | } 19 | 20 | get componentTreeView() { 21 | return this.componentTreeView$.asObservable(); 22 | } 23 | 24 | get nodeData() { 25 | return this.selectedNode.pipe(filter(Boolean), pluck('data')) 26 | } 27 | 28 | get updatedTreeId() { 29 | return this.updatedTreeId$.asObservable(); 30 | } 31 | 32 | selectNode(node) { 33 | this.selectedNode$.next(node); 34 | } 35 | 36 | updateTreeView(treeView: SerializedTreeViewItem) { 37 | this.componentTreeView$.next(treeView); 38 | } 39 | 40 | updateTreeId(id: string) { 41 | this.updatedTreeId$.next(id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/background.ts: -------------------------------------------------------------------------------- 1 | const connections = {}; 2 | 3 | chrome.runtime.onConnect.addListener(function (port) { 4 | const extensionListener = function (message, sender) { 5 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 6 | tabs[0] && chrome.tabs.sendMessage(tabs[0].id, message); 7 | }); 8 | }; 9 | connections[port.name] = port; 10 | 11 | // Listen to messages sent from the DevTools page 12 | port.onMessage.addListener(extensionListener); 13 | 14 | port.onDisconnect.addListener(function(port) { 15 | console.log(port, 'disconnect'); 16 | port.onMessage.removeListener(extensionListener); 17 | delete connections[port.name]; 18 | }); 19 | }); 20 | 21 | // Receive message from content script and relay to the devTools page for the 22 | // current tab 23 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 24 | // Messages from content scripts should have sender.tab set 25 | if (sender.tab) { 26 | const tabId = sender.tab.id; 27 | console.log(connections, tabId); 28 | if (tabId in connections) { 29 | connections[tabId].postMessage(request); 30 | } else { 31 | console.log("Tab not found in connection list."); 32 | } 33 | } else { 34 | console.log("sender.tab not defined."); 35 | } 36 | return true; 37 | }); -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/component-info/component-info.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from "@angular/forms"; 3 | import { Connection } from "../../../channel/connection"; 4 | import { MessageMethod, MessageType } from "@communication/message.type"; 5 | 6 | @Component({ 7 | selector: 'component-info', 8 | templateUrl: './component-info.component.html', 9 | styleUrls: ['./component-info.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class ComponentInfoComponent implements OnInit { 13 | @Input() 14 | get node() { 15 | return this._node; 16 | } 17 | set node(n) { 18 | this._node = n; 19 | this.form = this.fb.group(n.context); 20 | }; 21 | _node; 22 | form: FormGroup; 23 | constructor(private fb: FormBuilder, private connection: Connection) { 24 | } 25 | 26 | ngOnInit(): void { 27 | } 28 | 29 | checkType(value) { 30 | return typeof(value); 31 | } 32 | 33 | isEmpty(o) { 34 | return !Object.keys(o).length; 35 | } 36 | 37 | applyChanges() { 38 | this.connection.bgConnection.postMessage({ 39 | type: MessageType.APPLY_CHANGES, 40 | method: MessageMethod.Request, 41 | content: { 42 | ...this._node, 43 | context: this.form.value 44 | } 45 | }); 46 | } 47 | 48 | resetForm(e: MouseEvent): void { 49 | e.preventDefault(); 50 | this.form.reset(this.node.context); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { SplitPaneComponent } from "./split-pane/split-pane.component"; 4 | import { NzTabsModule } from "ng-zorro-antd/tabs"; 5 | import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions'; 6 | import { NzSwitchModule } from 'ng-zorro-antd/switch'; 7 | import { NzFormModule } from 'ng-zorro-antd/form'; 8 | import { NzLayoutModule } from "ng-zorro-antd/layout"; 9 | import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; 10 | import { NzButtonModule } from 'ng-zorro-antd/button'; 11 | import { NzIconModule } from 'ng-zorro-antd/icon'; 12 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 13 | import { NzCheckboxModule } from "ng-zorro-antd/checkbox"; 14 | import { NzTableModule } from "ng-zorro-antd"; 15 | 16 | const ZORRO_MODULES = [ 17 | NzTabsModule, 18 | NzDescriptionsModule, 19 | NzSwitchModule, 20 | NzFormModule, 21 | NzLayoutModule, 22 | NzInputNumberModule, 23 | NzButtonModule, 24 | NzIconModule, 25 | NzCheckboxModule, 26 | NzTableModule 27 | ]; 28 | 29 | const CUSTOM_COMPONENTS = [ 30 | SplitPaneComponent 31 | ]; 32 | 33 | @NgModule({ 34 | declarations: [ 35 | ...CUSTOM_COMPONENTS 36 | ], 37 | imports: [ 38 | CommonModule, 39 | FormsModule, 40 | ReactiveFormsModule, 41 | ...ZORRO_MODULES 42 | ], 43 | exports: [ 44 | FormsModule, 45 | ReactiveFormsModule, 46 | ...CUSTOM_COMPONENTS, 47 | ...ZORRO_MODULES 48 | ] 49 | }) 50 | export class SharedModule { } 51 | -------------------------------------------------------------------------------- /packages/devtools-page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtools-page", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "build:watch": "ng build --watch --deleteOutputPath=false", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~9.1.0", 16 | "@angular/cdk": "^9.2.0", 17 | "@angular/common": "~9.1.0", 18 | "@angular/compiler": "~9.1.0", 19 | "@angular/core": "~9.1.0", 20 | "@angular/forms": "~9.1.0", 21 | "@angular/platform-browser": "~9.1.0", 22 | "@angular/platform-browser-dynamic": "~9.1.0", 23 | "@angular/router": "~9.1.0", 24 | "d3": "^5.15.0", 25 | "ng-zorro-antd": "^9.0.1", 26 | "rxjs": "~6.5.4", 27 | "tslib": "^1.10.0", 28 | "zone.js": "~0.10.2" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~0.901.0", 32 | "@angular/cli": "~9.1.0", 33 | "@angular/compiler-cli": "~9.1.0", 34 | "@angular/language-service": "~9.1.0", 35 | "@types/jasmine": "~3.5.0", 36 | "@types/jasminewd2": "~2.0.3", 37 | "@types/node": "^12.11.1", 38 | "codelyzer": "^5.1.2", 39 | "jasmine-core": "~3.5.0", 40 | "jasmine-spec-reporter": "~4.2.1", 41 | "karma": "~4.4.1", 42 | "karma-chrome-launcher": "~3.1.0", 43 | "karma-coverage-istanbul-reporter": "~2.1.0", 44 | "karma-jasmine": "~3.0.1", 45 | "karma-jasmine-html-reporter": "^1.4.2", 46 | "protractor": "~5.4.3", 47 | "ts-node": "~8.3.0", 48 | "tslint": "~6.1.0", 49 | "typescript": "~3.8.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/type_checks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | 10 | import {LContainer, TYPE} from './container'; 11 | import {TNode, TNodeFlags} from './node'; 12 | import {RNode} from './renderer'; 13 | import {FLAGS, LView, LViewFlags} from './view'; 14 | import { ComponentDef, DirectiveDef } from "./definition"; 15 | 16 | 17 | /** 18 | * True if `value` is `LView`. 19 | * @param value wrapped value of `RNode`, `LView`, `LContainer` 20 | */ 21 | export function isLView(value: RNode | LView | LContainer | {} | null): value is LView { 22 | return Array.isArray(value) && typeof value[TYPE] === 'object'; 23 | } 24 | 25 | /** 26 | * True if `value` is `LContainer`. 27 | * @param value wrapped value of `RNode`, `LView`, `LContainer` 28 | */ 29 | export function isLContainer(value: RNode | LView | LContainer | {} | null): value is LContainer { 30 | return Array.isArray(value) && value[TYPE] === true; 31 | } 32 | 33 | export function isContentQueryHost(tNode: TNode): boolean { 34 | return (tNode.flags & TNodeFlags.hasContentQuery) !== 0; 35 | } 36 | 37 | export function isComponentHost(tNode: TNode): boolean { 38 | return (tNode.flags & TNodeFlags.isComponentHost) === TNodeFlags.isComponentHost; 39 | } 40 | 41 | export function isDirectiveHost(tNode: TNode): boolean { 42 | return (tNode.flags & TNodeFlags.isDirectiveHost) === TNodeFlags.isDirectiveHost; 43 | } 44 | 45 | export function isComponentDef(def: DirectiveDef): def is ComponentDef { 46 | return (def as ComponentDef).template !== null; 47 | } 48 | 49 | export function isRootView(target: LView): boolean { 50 | return (target[FLAGS] & LViewFlags.IsRoot) !== 0; 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular profiler 2 | 3 | Angular Profiler is a developer tool to power-up Angular development workflow by providing a series of debugging tools. 4 | 5 | It can be used as [Chrome Extension](https://chrome.google.com/webstore/detail/angular-profiler/ikkobdhhnnllhcmjaealbihjkahnegia) 6 | 7 | ![](images/ng-profiler.gif) 8 | 9 | ## How it can help us having a better developing experience && building a faster application 10 | 11 | ### Visual Change Detection and Component Tree 12 | Angular Profiler provides multiple ways to help us debugging over-triggered change detection in our Angular application. 13 | 14 | #### 1. Highlight the components that triggered CD and indicate CD times with different colors. 15 | 16 | ![](images/highlight.png) 17 | 18 | #### 2. Visual detecting changes in a component tree 19 | The component tree will show you how angular components are rendered and updated 20 | 21 | ![](images/component-tree.gif) 22 | 23 | ### Inspect and change component state 24 | Select a component in the component tree, then you can see all the states of this component in the `properties` tab. 25 | You can update the components right away without changing your codes and waiting webpack to compile to see your changes anymore. 26 | How awesome is that! :sunglasses: 27 | 28 | ![](images/state.png) 29 | 30 | ### Try it locally 31 | 32 | ```bash 33 | // init project and install dependences 34 | yarn 35 | // package plugin in watch mode 36 | yarn watch 37 | // package and compile devtools page app in watch mode 38 | yarn devtool-page:build:watch 39 | ``` 40 | The above commands will generate a `dist` folder that you can upload to chrome extension in development mode. 41 | 1. Go to `chrome://extensions/` 42 | 2. Turn on developer mode 43 | 3. Load unpacked `dist` 44 | 4. Enjoy a better developing experience :bowtie:! 45 | 5. Don't forget to send your feedback, all kinds of contributions are welcome. -------------------------------------------------------------------------------- /packages/core/angular/interfaces/context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | 10 | import {RNode} from './renderer'; 11 | import {LView} from './view'; 12 | 13 | /** 14 | * This property will be monkey-patched on elements, components and directives 15 | */ 16 | export const MONKEY_PATCH_KEY_NAME = '__ngContext__'; 17 | 18 | /** 19 | * The internal view context which is specific to a given DOM element, directive or 20 | * component instance. Each value in here (besides the LView and element node details) 21 | * can be present, null or undefined. If undefined then it implies the value has not been 22 | * looked up yet, otherwise, if null, then a lookup was executed and nothing was found. 23 | * 24 | * Each value will get filled when the respective value is examined within the getContext 25 | * function. The component, element and each directive instance will share the same instance 26 | * of the context. 27 | */ 28 | export interface LContext { 29 | /** 30 | * The component's parent view data. 31 | */ 32 | lView: LView; 33 | 34 | /** 35 | * The index instance of the node. 36 | */ 37 | nodeIndex: number; 38 | 39 | /** 40 | * The instance of the DOM node that is attached to the lNode. 41 | */ 42 | native: RNode; 43 | 44 | /** 45 | * The instance of the Component node. 46 | */ 47 | component: {}|null|undefined; 48 | 49 | /** 50 | * The list of active directives that exist on this element. 51 | */ 52 | directives: any[]|null|undefined; 53 | 54 | /** 55 | * The map of local references (local reference name => element or directive instance) that exist 56 | * on this element. 57 | */ 58 | localRefs: {[key: string]: any}|null|undefined; 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/angular/util/view_traversal_utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {isLContainer, isLView} from '../interfaces/type_checks'; 10 | import {CONTEXT, FLAGS, LView, LViewFlags, PARENT, RootContext} from '../interfaces/view'; 11 | import {readPatchedLView} from './view_utils'; 12 | 13 | 14 | /** 15 | * Gets the parent LView of the passed LView, if the PARENT is an LContainer, will get the parent of 16 | * that LContainer, which is an LView 17 | * @param lView the lView whose parent to get 18 | */ 19 | export function getLViewParent(lView: LView): LView|null { 20 | const parent = lView[PARENT]; 21 | return isLContainer(parent) ? parent[PARENT] ! : parent; 22 | } 23 | 24 | /** 25 | * Retrieve the root view from any component or `LView` by walking the parent `LView` until 26 | * reaching the root `LView`. 27 | * 28 | * @param componentOrLView any component or `LView` 29 | */ 30 | export function getRootView(componentOrLView: LView | {}): LView { 31 | let lView = isLView(componentOrLView) ? componentOrLView : readPatchedLView(componentOrLView) !; 32 | while (lView && !(lView[FLAGS] & LViewFlags.IsRoot)) { 33 | lView = getLViewParent(lView) !; 34 | } 35 | return lView; 36 | } 37 | 38 | /** 39 | * Returns the `RootContext` instance that is associated with 40 | * the application where the target is situated. It does this by walking the parent views until it 41 | * gets to the root view, then getting the context off of that. 42 | * 43 | * @param viewOrComponent the `LView` or component to get the root context for. 44 | */ 45 | export function getRootContext(viewOrComponent: LView | {}): RootContext { 46 | const rootView = getRootView(viewOrComponent); 47 | return rootView[CONTEXT] as RootContext; 48 | } 49 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/component-info/component-info.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{node.id}} 5 | {{node.name}} 6 | {{node.tagName}} 7 | 8 | 9 | 10 | {{node.parent}} 11 | 12 | 13 |
{{child.name}}
14 |
15 |
16 |
17 |
18 | 19 |
20 | 21 | {{state.key}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/document.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** 10 | * Most of the use of `document` in Angular is from within the DI system so it is possible to simply 11 | * inject the `DOCUMENT` token and are done. 12 | * 13 | * Ivy is special because it does not rely upon the DI and must get hold of the document some other 14 | * way. 15 | * 16 | * The solution is to define `getDocument()` and `setDocument()` top-level functions for ivy. 17 | * Wherever ivy needs the global document, it calls `getDocument()` instead. 18 | * 19 | * When running ivy outside of a browser environment, it is necessary to call `setDocument()` to 20 | * tell ivy what the global `document` is. 21 | * 22 | * Angular does this for us in each of the standard platforms (`Browser`, `Server`, and `WebWorker`) 23 | * by calling `setDocument()` when providing the `DOCUMENT` token. 24 | */ 25 | let DOCUMENT: Document|undefined = undefined; 26 | 27 | /** 28 | * Tell ivy what the `document` is for this platform. 29 | * 30 | * It is only necessary to call this if the current platform is not a browser. 31 | * 32 | * @param document The object representing the global `document` in this environment. 33 | */ 34 | export function setDocument(document: Document | undefined): void { 35 | DOCUMENT = document; 36 | } 37 | 38 | /** 39 | * Access the object that represents the `document` for this platform. 40 | * 41 | * Ivy calls this whenever it needs to access the `document` object. 42 | * For example to create the renderer or to do sanitization. 43 | */ 44 | export function getDocument(): Document { 45 | if (DOCUMENT !== undefined) { 46 | return DOCUMENT; 47 | } else if (typeof document !== 'undefined') { 48 | return document; 49 | } 50 | // No "document" can be found. This should only happen if we are running ivy outside Angular and 51 | // the current platform is not a browser. Since this is not a supported scenario at the moment 52 | // this should not happen in Angular apps. 53 | // Once we support running ivy outside of Angular we will need to publish `setDocument()` as a 54 | // public API. Meanwhile we just return `undefined` and let the application fail. 55 | return undefined !; 56 | } 57 | -------------------------------------------------------------------------------- /packages/popup/popup.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageMethod, MessageType } from "../communication/message.type"; 2 | import { AngularInfo } from "../core"; 3 | 4 | function onDOMContentLoaded() { 5 | const enableCheckBox = document.getElementById('enable'); 6 | const descrElem = document.getElementById('description'); 7 | const errorElem = document.getElementById('error'); 8 | const enableSwitch = document.getElementById('enableSwitch'); 9 | 10 | chrome.storage.local.get('ngProfilerEnabled', (content) => { 11 | (enableCheckBox as HTMLInputElement).checked = !!content.ngProfilerEnabled; 12 | }); 13 | 14 | enableCheckBox.addEventListener('change', e => { 15 | const enabled = (e.target as HTMLInputElement).checked; 16 | chrome.storage.local.set({ngProfilerEnabled: enabled}); 17 | chrome.browserAction.setIcon({ 18 | path: enabled ? { 19 | '16': 'images/icon16.png', 20 | '48': 'images/icon48.png', 21 | '128': 'images/icon128.png', 22 | } : { 23 | "16": "images/icon16_disabled.png", 24 | "48": "images/icon48_disabled.png", 25 | "148": "images/icon148_disabled.png" 26 | }, 27 | }); 28 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 29 | const message: Message = { 30 | type: MessageType.TOGGLE_PROFILING, 31 | method: MessageMethod.Request, 32 | content: enabled 33 | }; 34 | chrome.tabs.sendMessage(tabs[0].id, message); 35 | }); 36 | }); 37 | 38 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 39 | const message: Message = { 40 | type: MessageType.IS_IVY, 41 | method: MessageMethod.Request, 42 | }; 43 | try { 44 | chrome.tabs.sendMessage( 45 | tabs[0].id, 46 | message 47 | ); 48 | } catch (e) { 49 | console.error('Plugin probably disabled or something'); 50 | } 51 | }); 52 | 53 | chrome.runtime.onMessage.addListener( 54 | function (request: Message) { 55 | if (request.type === MessageType.IS_IVY) { 56 | if (!request.content.isIvy) { 57 | enableSwitch.style.display = 'none'; 58 | errorElem.innerText = `This page doesn't appear to be using IVY.`; 59 | } else { 60 | descrElem.innerText = `Found Angular on version ${request.content.version}`; 61 | } 62 | } 63 | } 64 | ); 65 | } 66 | 67 | document.addEventListener('DOMContentLoaded', onDOMContentLoaded); 68 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/component-tree.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MessageMethod, MessageType } from "@communication/message.type"; 3 | import { Connection } from "../../channel/connection"; 4 | import { debounceTime, tap } from "rxjs/operators"; 5 | import { COLORS, UPDATE_DEBOUNCE_TIME } from "@core/constants"; 6 | import { Subject } from "rxjs"; 7 | import * as d3 from 'd3'; 8 | import { ViewService } from "../../core/view.service"; 9 | 10 | @Component({ 11 | selector: 'component-tree', 12 | templateUrl: './component-tree.component.html', 13 | styleUrls: ['./component-tree.component.css'] 14 | }) 15 | export class ComponentTreeComponent implements OnInit { 16 | drawingPool: Subject = new Subject(); 17 | nodeMap = new Map(); 18 | 19 | constructor(private connection: Connection, public viewService: ViewService) { } 20 | 21 | ngOnInit() { 22 | this.viewService.updatedTreeId 23 | .subscribe(id => { 24 | if (this.nodeMap.has(id)) { 25 | const previous = this.nodeMap.get(id); 26 | this.nodeMap.set(id, {rect: previous.rect, link: previous.link, time: previous.time + 1}); 27 | } else { 28 | this.nodeMap.set(id, {rect: d3.select(`#r${id}`), link: d3.select(`#l${id}`), time: 1}); 29 | } 30 | this.drawingPool.next(id); 31 | }) 32 | this.drawingPool.pipe( 33 | tap(this.updateRect), 34 | debounceTime(UPDATE_DEBOUNCE_TIME), 35 | ).subscribe(() => { 36 | this.resetRect(); 37 | }) 38 | } 39 | hoverNode = (id: string) => { 40 | this.connection.bgConnection.postMessage({ 41 | type: MessageType.HIGHLIGHT_ELEMENT, 42 | method: MessageMethod.Request, 43 | content: id 44 | }); 45 | }; 46 | 47 | updateRect = (id: string) => { 48 | const node = this.nodeMap.get(id); 49 | const color = COLORS[Math.ceil(node.time / 2) - 1] || COLORS[COLORS.length - 1]; 50 | node.rect.style('fill', () => color); 51 | node.link 52 | .attr('stroke', () => color) 53 | .attr('stroke-opacity', 1) 54 | .attr('stroke-width', 5); 55 | }; 56 | 57 | resetRect = () => { 58 | this.nodeMap.forEach(({rect, link}, key, map) => { 59 | rect.style('fill', (d) => d._children ? 'lightsteelblue' : '#fff'); 60 | link.attr('stroke', '#555') 61 | .attr('stroke-opacity', 0.6) 62 | .attr('stroke-width', 1.5); 63 | map.set(key, {rect, link, time: 1}) 64 | }); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/projection.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @license 4 | * Copyright Google Inc. All Rights Reserved. 5 | * 6 | * Use of this source code is governed by an MIT-style license that can be 7 | * found in the LICENSE file at https://angular.io/license 8 | */ 9 | 10 | 11 | /** 12 | * Expresses a single CSS Selector. 13 | * 14 | * Beginning of array 15 | * - First index: element name 16 | * - Subsequent odd indices: attr keys 17 | * - Subsequent even indices: attr values 18 | * 19 | * After SelectorFlags.CLASS flag 20 | * - Class name values 21 | * 22 | * SelectorFlags.NOT flag 23 | * - Changes the mode to NOT 24 | * - Can be combined with other flags to set the element / attr / class mode 25 | * 26 | * e.g. SelectorFlags.NOT | SelectorFlags.ELEMENT 27 | * 28 | * Example: 29 | * Original: `div.foo.bar[attr1=val1][attr2]` 30 | * Parsed: ['div', 'attr1', 'val1', 'attr2', '', SelectorFlags.CLASS, 'foo', 'bar'] 31 | * 32 | * Original: 'div[attr1]:not(.foo[attr2]) 33 | * Parsed: [ 34 | * 'div', 'attr1', '', 35 | * SelectorFlags.NOT | SelectorFlags.ATTRIBUTE 'attr2', '', SelectorFlags.CLASS, 'foo' 36 | * ] 37 | * 38 | * See more examples in node_selector_matcher_spec.ts 39 | */ 40 | export type CssSelector = (string | SelectorFlags)[]; 41 | 42 | /** 43 | * A list of CssSelectors. 44 | * 45 | * A directive or component can have multiple selectors. This type is used for 46 | * directive defs so any of the selectors in the list will match that directive. 47 | * 48 | * Original: 'form, [ngForm]' 49 | * Parsed: [['form'], ['', 'ngForm', '']] 50 | */ 51 | export type CssSelectorList = CssSelector[]; 52 | 53 | /** 54 | * List of slots for a projection. A slot can be either based on a parsed CSS selector 55 | * which will be used to determine nodes which are projected into that slot. 56 | * 57 | * When set to "*", the slot is reserved and can be used for multi-slot projection 58 | * using {@link ViewContainerRef#createComponent}. The last slot that specifies the 59 | * wildcard selector will retrieve all projectable nodes which do not match any selector. 60 | */ 61 | export type ProjectionSlots = (CssSelectorList | '*')[]; 62 | 63 | /** Flags used to build up CssSelectors */ 64 | export const enum SelectorFlags { 65 | /** Indicates this is the beginning of a new negative selector */ 66 | NOT = 0b0001, 67 | 68 | /** Mode for matching attributes */ 69 | ATTRIBUTE = 0b0010, 70 | 71 | /** Mode for matching tag names */ 72 | ELEMENT = 0b0100, 73 | 74 | /** Mode for matching class names */ 75 | CLASS = 0b1000, 76 | } 77 | 78 | // Note: This hack is necessary so we don't erroneously get a circular dependency 79 | // failure based on types. 80 | export const unusedValueExportToPlacateAjd = 1; 81 | -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | import { Message, MESSAGE_SOURCE, MessageMethod, MessageType } from "../communication/message.type"; 2 | import { findAngularVersion } from "./angular/util/view_utils"; 3 | import { createMessage } from "../communication/messager"; 4 | import { patchComponentTree, startProfiling, stopProfiling } from "./util/profiling"; 5 | import { TreeViewFactory } from "./util/treeView"; 6 | import { CanvasFactory } from "./util/canvas"; 7 | import { scheduleOutsideOfZone } from "./util/zone"; 8 | 9 | export interface AngularInfo { 10 | isIvy: boolean, 11 | version: string 12 | } 13 | 14 | function handleMessage(e: MessageEvent) { 15 | if (!e.data) { 16 | return; 17 | } 18 | 19 | const data: Message = e.data; 20 | 21 | if (!data || data.source !== MESSAGE_SOURCE || data.method !== MessageMethod.Request) { 22 | return; 23 | } 24 | 25 | let content: AngularInfo | any; 26 | const view = TreeViewFactory.bodyLView; 27 | if (data.type === MessageType.IS_IVY) { 28 | content = { 29 | isIvy: !!view, 30 | version: findAngularVersion(view) 31 | } 32 | } else if (data.type === MessageType.TOGGLE_PROFILING) { 33 | data.content && !!view ? startProfiling() : stopProfiling(); 34 | } else if (data.type === MessageType.COMPONENT_TREE) { 35 | content = TreeViewFactory.serialisedTreeView; 36 | } else if (data.type === MessageType.APPLY_CHANGES) { 37 | TreeViewFactory.applyChanges(data.content); 38 | } else if (data.type === MessageType.HIGHLIGHT_ELEMENT) { 39 | scheduleOutsideOfZone(() => { 40 | if (data.content) { 41 | CanvasFactory.highlight(TreeViewFactory.treeLViewMap.get(data.content)); 42 | } else { 43 | CanvasFactory.clear(); 44 | } 45 | }); 46 | } else if (data.type === MessageType.HIGHLIGHT_VIEW) { 47 | if (data.content !== undefined) { 48 | TreeViewFactory.highlightView = data.content; 49 | } 50 | content = TreeViewFactory.highlightView; 51 | } else if (data.type === MessageType.HIGHLIGHT_TREE) { 52 | if (data.content !== undefined) { 53 | TreeViewFactory.highlightTree = data.content; 54 | } 55 | content = TreeViewFactory.highlightTree; 56 | } 57 | 58 | postMessage(createMessage(data.type, MessageMethod.Response, content), '*'); 59 | } 60 | 61 | window.addEventListener('message', handleMessage); 62 | 63 | const listener = (type: string) => { 64 | const origin = history[type]; 65 | return function () { 66 | if (TreeViewFactory.bodyLView && TreeViewFactory.enabled) { 67 | setTimeout(() => { 68 | patchComponentTree((serialisedTreeView) => { 69 | postMessage(createMessage(MessageType.COMPONENT_TREE, MessageMethod.Response, serialisedTreeView), '*'); 70 | }); 71 | }); 72 | } 73 | return origin.apply(this, arguments) 74 | } 75 | }; 76 | 77 | window.history.pushState = listener('pushState'); 78 | window.history.replaceState = listener('replaceState'); -------------------------------------------------------------------------------- /packages/devtools-page/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgZone, OnInit } from '@angular/core'; 2 | import { Message, MessageMethod, MessageType } from "@communication/message.type"; 3 | import { Connection } from "@devtools-page/channel/connection"; 4 | import { ViewProfile } from "@core/util/treeView"; 5 | import { ViewService } from "@devtools-page/core/view.service"; 6 | import { SerializedTreeViewItem } from "@devtools-page/containers/component-tree/tree-diagram/tree-diagram.component"; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.scss'] 12 | }) 13 | export class AppComponent implements OnInit { 14 | highlightView: boolean; 15 | highlightTree: boolean; 16 | 17 | constructor(private connection: Connection, public viewService: ViewService, private zone: NgZone) {} 18 | 19 | ngOnInit() { 20 | this.connection.bgConnection.postMessage({ 21 | type: MessageType.COMPONENT_TREE, 22 | method: MessageMethod.Request, 23 | }); 24 | // TODO: clean this shit 25 | this.connection.bgConnection.onMessage.addListener((message: Message) => { 26 | if (message.method === MessageMethod.Request) { 27 | if (message.type === MessageType.TOGGLE_PROFILING) { 28 | if (message.content) { 29 | this.connection.bgConnection.postMessage({ 30 | type: MessageType.COMPONENT_TREE, 31 | method: MessageMethod.Request, 32 | }); 33 | } else { 34 | this.viewService.updateTreeView(null); 35 | } 36 | } 37 | } else if (message.method === MessageMethod.Response) { 38 | if (message.type === MessageType.HIGHLIGHT_VIEW) { 39 | this.zone.run(() => {this.highlightView = message.content}); 40 | } 41 | if (message.type === MessageType.HIGHLIGHT_TREE) { 42 | this.zone.run(() => {this.highlightTree = message.content}); 43 | } 44 | if (message.type === MessageType.COMPONENT_TREE) { 45 | console.log(message.content); 46 | this.zone.run(() => {this.viewService.updateTreeView(message.content);}); 47 | } 48 | if (message.type === MessageType.UPDATE_TREE) { 49 | const id = message.content; 50 | this.viewService.updateTreeId(id); 51 | } 52 | 53 | if (message.type === MessageType.VIEW_PROFILES) { 54 | // this.profile.addProfile(message.content as ViewProfile); 55 | } 56 | } 57 | }) 58 | } 59 | 60 | updateHighlightView(content) { 61 | this.connection.bgConnection.postMessage({ 62 | type: MessageType.HIGHLIGHT_VIEW, 63 | method: MessageMethod.Request, 64 | content 65 | }); 66 | } 67 | 68 | updateHighlightTree(content) { 69 | this.connection.bgConnection.postMessage({ 70 | type: MessageType.HIGHLIGHT_TREE, 71 | method: MessageMethod.Request, 72 | content 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/core/util/canvas.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "rxjs"; 2 | import { debounceTime, tap } from "rxjs/operators"; 3 | import { COLORS, DRAWER_THRESHOLD, UPDATE_DEBOUNCE_TIME } from "../constants"; 4 | import { HOST, LView } from "../angular/interfaces/view"; 5 | import { TreeViewFactory } from "./treeView"; 6 | import { scheduleOutsideOfZone } from "./zone"; 7 | 8 | class Canvas { 9 | canvas: HTMLCanvasElement; 10 | drawingPool: Subject<{uuid: string, rect: DOMRect}> = new Subject(); 11 | hostMap = new Map(); 12 | 13 | constructor() { 14 | scheduleOutsideOfZone(() => { 15 | this.drawingPool.pipe( 16 | tap(this.drawborder), 17 | debounceTime(UPDATE_DEBOUNCE_TIME) 18 | ).subscribe(() => { 19 | this.clear(); 20 | }) 21 | }); 22 | } 23 | create = () => { 24 | if (this.canvas) return ; 25 | const canvas = document.createElement('canvas') as HTMLCanvasElement; 26 | canvas.width = window.screen.availWidth; 27 | canvas.height = window.screen.availHeight; 28 | canvas.style.cssText = ` 29 | bottom: 0; 30 | left: 0; 31 | pointer-events: none; 32 | position: fixed; 33 | right: 0; 34 | top: 0; 35 | z-index: 1000000000; 36 | `; 37 | const root = document.documentElement; 38 | root.insertBefore(canvas, root.firstChild); 39 | this.canvas = canvas; 40 | }; 41 | 42 | highlight = (lView: LView) => { 43 | if (!lView) return ; 44 | const rect: DOMRect = lView[HOST]!?.getBoundingClientRect(); 45 | const ctx = this.canvas.getContext('2d'); 46 | ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 47 | ctx.fillStyle = 'rgba(60, 210, 240, 0.3)'; 48 | ctx.fillRect(rect.x, rect.y, rect.width, rect.height); 49 | ctx.font = "1rem Arial"; 50 | ctx.textAlign = "right"; 51 | ctx.fillStyle = "#343ad7"; 52 | ctx.fillText(TreeViewFactory.getComponentName(lView), rect.x + rect.width - 10, rect.y + rect.height - 10); 53 | }; 54 | 55 | draw = (uuid: string, rect: DOMRect) => { 56 | if (this.hostMap.has(uuid)) { 57 | this.hostMap.set(uuid, this.hostMap.get(uuid) + 1); 58 | } else { 59 | this.hostMap.set(uuid, 1); 60 | } 61 | if (this.hostMap.get(uuid) < DRAWER_THRESHOLD) { 62 | this.drawingPool.next({uuid, rect}); 63 | } 64 | }; 65 | 66 | drawborder = ({uuid, rect}: {uuid: string, rect: DOMRect}) => { 67 | if (rect.width === 0 && rect.height === 0) return ; 68 | 69 | const ctx = this.canvas.getContext("2d"); 70 | const renderTime = this.hostMap.get(uuid); 71 | ctx.strokeStyle = COLORS[Math.ceil(renderTime/2) - 1]; 72 | ctx.strokeRect( 73 | rect.x + Math.floor(renderTime / 2), 74 | rect.y + Math.floor(renderTime / 2), 75 | rect.width - renderTime, 76 | rect.height - renderTime 77 | ); 78 | }; 79 | 80 | clear() { 81 | this.hostMap.clear(); 82 | const ctx = this.canvas.getContext("2d"); 83 | ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 84 | } 85 | } 86 | 87 | export const CanvasFactory = new Canvas(); -------------------------------------------------------------------------------- /packages/devtools-page/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 101 |

Angular Profiler

102 |

103 |
104 | 105 | 106 |
107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /packages/devtools-page/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-return-shorthand": true, 12 | "curly": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "directive-class-suffix": true, 19 | "directive-selector": [ 20 | true, 21 | "attribute", 22 | "app", 23 | "camelCase" 24 | ], 25 | "component-selector": [ 26 | true, 27 | "element", 28 | "app", 29 | "kebab-case" 30 | ], 31 | "eofline": true, 32 | "import-blacklist": [ 33 | true, 34 | "rxjs/Rx" 35 | ], 36 | "import-spacing": true, 37 | "indent": { 38 | "options": [ 39 | "spaces" 40 | ] 41 | }, 42 | "max-classes-per-file": false, 43 | "max-line-length": [ 44 | true, 45 | 140 46 | ], 47 | "member-ordering": [ 48 | true, 49 | { 50 | "order": [ 51 | "static-field", 52 | "instance-field", 53 | "static-method", 54 | "instance-method" 55 | ] 56 | } 57 | ], 58 | "no-console": [ 59 | true, 60 | "debug", 61 | "info", 62 | "time", 63 | "timeEnd", 64 | "trace" 65 | ], 66 | "no-empty": false, 67 | "no-inferrable-types": [ 68 | true, 69 | "ignore-params" 70 | ], 71 | "no-non-null-assertion": true, 72 | "no-redundant-jsdoc": true, 73 | "no-switch-case-fall-through": true, 74 | "no-var-requires": false, 75 | "object-literal-key-quotes": [ 76 | true, 77 | "as-needed" 78 | ], 79 | "quotemark": [ 80 | true, 81 | "single" 82 | ], 83 | "semicolon": { 84 | "options": [ 85 | "always" 86 | ] 87 | }, 88 | "space-before-function-paren": { 89 | "options": { 90 | "anonymous": "never", 91 | "asyncArrow": "always", 92 | "constructor": "never", 93 | "method": "never", 94 | "named": "never" 95 | } 96 | }, 97 | "typedef-whitespace": { 98 | "options": [ 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | }, 106 | { 107 | "call-signature": "onespace", 108 | "index-signature": "onespace", 109 | "parameter": "onespace", 110 | "property-declaration": "onespace", 111 | "variable-declaration": "onespace" 112 | } 113 | ] 114 | }, 115 | "variable-name": { 116 | "options": [ 117 | "ban-keywords", 118 | "check-format", 119 | "allow-pascal-case" 120 | ] 121 | }, 122 | "whitespace": { 123 | "options": [ 124 | "check-branch", 125 | "check-decl", 126 | "check-operator", 127 | "check-separator", 128 | "check-type", 129 | "check-typecast" 130 | ] 131 | }, 132 | "no-conflicting-lifecycle": true, 133 | "no-host-metadata-property": true, 134 | "no-input-rename": true, 135 | "no-inputs-metadata-property": true, 136 | "no-output-native": true, 137 | "no-output-on-prefix": true, 138 | "no-output-rename": true, 139 | "no-outputs-metadata-property": true, 140 | "template-banana-in-box": true, 141 | "template-no-negated-async": true, 142 | "use-lifecycle-interface": true, 143 | "use-pipe-transform-interface": true 144 | }, 145 | "rulesDirectory": [ 146 | "codelyzer" 147 | ] 148 | } -------------------------------------------------------------------------------- /packages/devtools-page/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "devtools-page": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "../../dist", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "aot": true, 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets", 25 | { 26 | "glob": "**/*", 27 | "input": "../../node_modules/@ant-design/icons-angular/src/inline-svg/", 28 | "output": "/assets/" 29 | } 30 | ], 31 | "styles": [ 32 | "../../node_modules/ng-zorro-antd/ng-zorro-antd.min.css", 33 | "src/styles.css" 34 | ], 35 | "scripts": [] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "extractCss": true, 49 | "namedChunks": false, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "6kb", 62 | "maximumError": "10kb" 63 | } 64 | ] 65 | } 66 | } 67 | }, 68 | "serve": { 69 | "builder": "@angular-devkit/build-angular:dev-server", 70 | "options": { 71 | "browserTarget": "devtools-page:build" 72 | }, 73 | "configurations": { 74 | "production": { 75 | "browserTarget": "devtools-page:build:production" 76 | } 77 | } 78 | }, 79 | "extract-i18n": { 80 | "builder": "@angular-devkit/build-angular:extract-i18n", 81 | "options": { 82 | "browserTarget": "devtools-page:build" 83 | } 84 | }, 85 | "test": { 86 | "builder": "@angular-devkit/build-angular:karma", 87 | "options": { 88 | "main": "src/test.ts", 89 | "polyfills": "src/polyfills.ts", 90 | "tsConfig": "tsconfig.spec.json", 91 | "karmaConfig": "karma.conf.js", 92 | "assets": [ 93 | "src/favicon.ico", 94 | "src/assets" 95 | ], 96 | "styles": [ 97 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 98 | "src/styles.css" 99 | ], 100 | "scripts": [] 101 | } 102 | }, 103 | "lint": { 104 | "builder": "@angular-devkit/build-angular:tslint", 105 | "options": { 106 | "tsConfig": [ 107 | "tsconfig.app.json", 108 | "tsconfig.spec.json", 109 | "e2e/tsconfig.json" 110 | ], 111 | "exclude": [ 112 | "**/node_modules/**" 113 | ] 114 | } 115 | }, 116 | "e2e": { 117 | "builder": "@angular-devkit/build-angular:protractor", 118 | "options": { 119 | "protractorConfig": "e2e/protractor.conf.js", 120 | "devServerTarget": "devtools-page:serve" 121 | }, 122 | "configurations": { 123 | "production": { 124 | "devServerTarget": "devtools-page:serve:production" 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "defaultProject": "devtools-page" 132 | } 133 | -------------------------------------------------------------------------------- /packages/content-script.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageMethod, MessageType } from "./communication/message.type"; 2 | import { createMessage, observeMessage, observeResponse } from "./communication/messager"; 3 | import { AngularInfo } from "./core"; 4 | import { filter } from "rxjs/operators"; 5 | import { SerializedTreeViewItem, ViewProfile } from "@core/util/treeView"; 6 | 7 | const scriptInjection = new Set(); 8 | 9 | const inject = (fn: (element: HTMLScriptElement) => void) => { 10 | const script = document.createElement('script') as HTMLScriptElement; 11 | fn(script); 12 | document.documentElement.appendChild(script); 13 | script.parentNode.removeChild(script); 14 | }; 15 | 16 | const injectScript = (path: string, onLoadHandler?: () => void) => { 17 | if (scriptInjection.has(path)) { 18 | return; 19 | } 20 | 21 | inject(script => { 22 | script.src = chrome.extension.getURL(path); 23 | if (onLoadHandler) { 24 | script.onload = onLoadHandler; 25 | } 26 | }); 27 | 28 | scriptInjection.add(path); 29 | }; 30 | 31 | const onInjectedScriptLoaded = () => { 32 | chrome.storage.local.get('ngProfilerEnabled', (data) => { 33 | handler[MessageType.TOGGLE_PROFILING]({content: data.ngProfilerEnabled}); 34 | }); 35 | chrome.storage.local.get(MessageType.HIGHLIGHT_VIEW, (storage) => { 36 | handler[MessageType.HIGHLIGHT_VIEW]({content: storage[MessageType.HIGHLIGHT_VIEW]}) 37 | }); 38 | chrome.storage.local.get(MessageType.HIGHLIGHT_TREE, (storage) => { 39 | handler[MessageType.HIGHLIGHT_TREE]({content: storage[MessageType.HIGHLIGHT_TREE]}) 40 | }); 41 | }; 42 | 43 | injectScript('core.bundle.js', onInjectedScriptLoaded); 44 | 45 | // listen from popup and devtool page 46 | chrome.runtime.onMessage.addListener( (request: Message) => { 47 | if (request.method !== MessageMethod.Request) return; 48 | handler[request.type](request); 49 | }); 50 | 51 | // listen core/index and send to background -> devtool page 52 | observeResponse(MessageType.UPDATE_TREE).pipe(filter(id => !!id)).subscribe(componentId => { 53 | chrome.runtime.sendMessage(createMessage(MessageType.UPDATE_TREE, MessageMethod.Response, componentId)) 54 | }); 55 | 56 | observeResponse(MessageType.VIEW_PROFILES).subscribe(profiles => { 57 | chrome.runtime.sendMessage(createMessage(MessageType.VIEW_PROFILES, MessageMethod.Response, profiles)) 58 | }); 59 | 60 | const handler = { 61 | [MessageType.IS_IVY]: () => { 62 | observeMessage( 63 | createMessage(MessageType.IS_IVY, MessageMethod.Request) 64 | ).subscribe(info => { 65 | chrome.runtime.sendMessage(createMessage(MessageType.IS_IVY, MessageMethod.Response, info)) 66 | }); 67 | }, 68 | [MessageType.TOGGLE_PROFILING]: (request: {content: boolean}) => { 69 | // send to core/index 70 | observeMessage( 71 | createMessage(MessageType.TOGGLE_PROFILING, MessageMethod.Request, request.content) 72 | ); 73 | // send to background -> devtool page 74 | chrome.runtime.sendMessage(createMessage(MessageType.TOGGLE_PROFILING, MessageMethod.Request, request.content)); 75 | }, 76 | [MessageType.COMPONENT_TREE]: () => { 77 | observeMessage( 78 | createMessage(MessageType.COMPONENT_TREE, MessageMethod.Request) 79 | ).subscribe(tree => { 80 | chrome.runtime.sendMessage(createMessage(MessageType.COMPONENT_TREE, MessageMethod.Response, tree)) 81 | }); 82 | }, 83 | [MessageType.APPLY_CHANGES]: (request) => { 84 | observeMessage( 85 | createMessage(MessageType.APPLY_CHANGES, MessageMethod.Request, request.content) 86 | ) 87 | }, 88 | [MessageType.HIGHLIGHT_ELEMENT]: (request) => { 89 | observeMessage( 90 | createMessage(MessageType.HIGHLIGHT_ELEMENT, MessageMethod.Request, request.content) 91 | ) 92 | }, 93 | [MessageType.HIGHLIGHT_VIEW]: (request) => { 94 | request.content !== undefined && chrome.storage.local.set({[MessageType.HIGHLIGHT_VIEW]: request.content}); 95 | observeMessage( 96 | createMessage(MessageType.HIGHLIGHT_VIEW, MessageMethod.Request, request.content) 97 | ).subscribe(status => { 98 | chrome.runtime.sendMessage(createMessage(MessageType.HIGHLIGHT_VIEW, MessageMethod.Response, status)); 99 | }); 100 | }, 101 | [MessageType.HIGHLIGHT_TREE]: (request) => { 102 | request.content !== undefined && chrome.storage.local.set({[MessageType.HIGHLIGHT_TREE]: request.content}); 103 | observeMessage( 104 | createMessage(MessageType.HIGHLIGHT_TREE, MessageMethod.Request, request.content) 105 | ).subscribe(status => { 106 | chrome.runtime.sendMessage(createMessage(MessageType.HIGHLIGHT_TREE, MessageMethod.Response, status)); 107 | }); 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/player.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** 10 | * A shared interface which contains an animation player 11 | */ 12 | export interface Player { 13 | parent?: Player|null; 14 | state: PlayState; 15 | play(): void; 16 | pause(): void; 17 | finish(): void; 18 | destroy(): void; 19 | addEventListener(state: PlayState|string, cb: (data?: any) => any): void; 20 | } 21 | 22 | export const enum BindingType { 23 | Unset = 0, 24 | Class = 1, 25 | Style = 2, 26 | } 27 | 28 | export interface BindingStore { setValue(prop: string, value: any): void; } 29 | 30 | /** 31 | * Defines the shape which produces the Player. 32 | * 33 | * Used to produce a player that will be placed on an element that contains 34 | * styling bindings that make use of the player. This function is designed 35 | * to be used with `PlayerFactory`. 36 | */ 37 | export interface PlayerFactoryBuildFn { 38 | (element: HTMLElement, type: BindingType, values: {[key: string]: any}, isFirstRender: boolean, 39 | currentPlayer: Player|null): Player|null; 40 | } 41 | 42 | /** 43 | * Used as a reference to build a player from a styling template binding 44 | * (`[style]` and `[class]`). 45 | * 46 | * The `fn` function will be called once any styling-related changes are 47 | * evaluated on an element and is expected to return a player that will 48 | * be then run on the element. 49 | * 50 | * `[style]`, `[style.prop]`, `[class]` and `[class.name]` template bindings 51 | * all accept a `PlayerFactory` as input and this player factories. 52 | */ 53 | export interface PlayerFactory { '__brand__': 'Brand for PlayerFactory that nothing will match'; } 54 | 55 | export interface PlayerBuilder extends BindingStore { 56 | buildPlayer(currentPlayer: Player|null, isFirstRender: boolean): Player|undefined|null; 57 | } 58 | 59 | /** 60 | * The state of a given player 61 | * 62 | * Do not change the increasing nature of the numbers since the player 63 | * code may compare state by checking if a number is higher or lower than 64 | * a certain numeric value. 65 | */ 66 | export const enum PlayState {Pending = 0, Running = 1, Paused = 2, Finished = 100, Destroyed = 200} 67 | 68 | /** 69 | * The context that stores all the active players and queued player factories present on an element. 70 | */ 71 | export interface PlayerContext extends Array { 72 | [PlayerIndex.NonBuilderPlayersStart]: number; 73 | [PlayerIndex.ClassMapPlayerBuilderPosition]: PlayerBuilder|null; 74 | [PlayerIndex.ClassMapPlayerPosition]: Player|null; 75 | [PlayerIndex.StyleMapPlayerBuilderPosition]: PlayerBuilder|null; 76 | [PlayerIndex.StyleMapPlayerPosition]: Player|null; 77 | } 78 | 79 | /** 80 | * Designed to be used as an injection service to capture all animation players. 81 | * 82 | * When present all animation players will be passed into the flush method below. 83 | * This feature is designed to service application-wide animation testing, live 84 | * debugging as well as custom animation choreographing tools. 85 | */ 86 | export interface PlayerHandler { 87 | /** 88 | * Designed to kick off the player at the end of change detection 89 | */ 90 | flushPlayers(): void; 91 | 92 | /** 93 | * @param player The player that has been scheduled to run within the application. 94 | * @param context The context as to where the player was bound to 95 | */ 96 | queuePlayer(player: Player, context: ComponentInstance|DirectiveInstance|HTMLElement): void; 97 | } 98 | 99 | export const enum PlayerIndex { 100 | // The position where the index that reveals where players start in the PlayerContext 101 | NonBuilderPlayersStart = 0, 102 | // The position where the player builder lives (which handles {key:value} map expression) for 103 | // classes 104 | ClassMapPlayerBuilderPosition = 1, 105 | // The position where the last player assigned to the class player builder is stored 106 | ClassMapPlayerPosition = 2, 107 | // The position where the player builder lives (which handles {key:value} map expression) for 108 | // styles 109 | StyleMapPlayerBuilderPosition = 3, 110 | // The position where the last player assigned to the style player builder is stored 111 | StyleMapPlayerPosition = 4, 112 | // The position where any player builders start in the PlayerContext 113 | PlayerBuildersStartPosition = 1, 114 | // The position where non map-based player builders start in the PlayerContext 115 | SinglePlayerBuildersStartPosition = 5, 116 | // For each player builder there is a player in the player context (therefore size = 2) 117 | PlayerAndPlayerBuildersTupleSize = 2, 118 | // The player exists next to the player builder in the list 119 | PlayerOffsetPosition = 1, 120 | } 121 | 122 | export declare type ComponentInstance = {}; 123 | export declare type DirectiveInstance = {}; 124 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/shared/split-pane/split-pane.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * component. 3 | * 4 | * This may seem like a mess, but there is no other way to implement resizable panes. 5 | */ 6 | 7 | import { 8 | Component, 9 | ElementRef, 10 | ViewChild, 11 | } from '@angular/core'; 12 | 13 | // This value matches the behaviour in the Chrome Dev Tools 14 | const MIN_PANE_WIDTH = 26; 15 | 16 | const DEFAULT_SECONDARY_WIDTH = 384; 17 | 18 | @Component({ 19 | selector: 'split-pane', 20 | templateUrl: './split-pane.component.html', 21 | styleUrls: ['./split-pane.component.scss'] 22 | }) 23 | export class SplitPaneComponent { 24 | @ViewChild('wrapper', { static: false }) wrapperElement: ElementRef; 25 | @ViewChild('resizer', { static: false }) resizerElement: ElementRef; 26 | @ViewChild('overlay', { static: false }) overlayElement: ElementRef; 27 | @ViewChild('primary', { static: false }) primaryElement: ElementRef; 28 | @ViewChild('secondary', { static: false }) secondaryElement: ElementRef; 29 | 30 | // TODO: store the initial secondary pane width in a preference. 31 | secondaryWidth = DEFAULT_SECONDARY_WIDTH; 32 | 33 | // State for resizing 34 | mouseX: number; 35 | bounds: ClientRect; 36 | guardMouseMoved; 37 | guardMouseUp; 38 | 39 | /** 40 | * Clamp the secondary panel width value to prevent layout issues. 41 | */ 42 | clampSecondaryWidth() { 43 | const minSecondaryWidth = this.secondaryWidth < MIN_PANE_WIDTH ? 44 | MIN_PANE_WIDTH : 45 | this.secondaryWidth; 46 | const clampedSecondaryWidth = this.bounds.width - minSecondaryWidth < MIN_PANE_WIDTH ? 47 | this.bounds.width - MIN_PANE_WIDTH : 48 | minSecondaryWidth; 49 | return clampedSecondaryWidth; 50 | } 51 | 52 | /** 53 | * When the component initializes, capture the initial geometry, 54 | * and set up the resizer element. 55 | */ 56 | ngAfterViewInit() { 57 | // Proxy event handlers to preserve instance binding. 58 | this.guardMouseMoved = (e) => this.mouseMoved(e); 59 | this.guardMouseUp = (e) => this.mouseUp(e); 60 | 61 | this.windowResized(); 62 | } 63 | 64 | /** 65 | * Automatically resize window panes after tab change 66 | */ 67 | handleTabNavigation() { 68 | setTimeout(this.windowResized.bind(this)); 69 | } 70 | 71 | /** 72 | * Capture the starting mouse position, and set up 73 | * the mouse move capture div for a resize drag. 74 | */ 75 | resizerMouseDown($event) { 76 | this.secondaryWidth = this.clampSecondaryWidth(); 77 | 78 | this.mouseX = $event.clientX; 79 | 80 | // Required to convince TypeScript that there is a WebkitUserSelect CSS property. 81 | const bodyStyle: any = window.document.body.style; 82 | bodyStyle.WebkitUserSelect = 'none'; 83 | 84 | this.overlayElement.nativeElement.style.display = 'block'; 85 | 86 | window.addEventListener('mousemove', this.guardMouseMoved); 87 | window.addEventListener('mouseup', this.guardMouseUp); 88 | } 89 | 90 | /** 91 | * Handle mouse move events on window. 92 | */ 93 | mouseMoved(e) { 94 | this.secondaryWidth = (this.mouseX - e.clientX) + this.secondaryWidth; 95 | this.mouseX = e.clientX; 96 | 97 | this.reshape(); 98 | } 99 | 100 | /** 101 | * Finalize a resize drag, and set secondaryWidth 102 | * to reflect what is currently rendered. 103 | */ 104 | mouseUp(e) { 105 | // Required to convince TypeScript that there is a WebkitUserSelect CSS property. 106 | const bodyStyle: any = window.document.body.style; 107 | bodyStyle.WebkitUserSelect = ''; 108 | 109 | this.overlayElement.nativeElement.style.display = 'none'; 110 | 111 | window.removeEventListener('mousemove', this.guardMouseMoved); 112 | window.removeEventListener('mouseup', this.guardMouseUp); 113 | 114 | this.secondaryWidth = this.clampSecondaryWidth(); 115 | } 116 | 117 | /** 118 | * Reshape the resizer, primary pane, and secondary pane 119 | * based on current component geometry and panel width. 120 | */ 121 | reshape() { 122 | const clampedSecondaryWidth = this.clampSecondaryWidth(); 123 | 124 | // Set the primary pane width. 125 | this.primaryElement.nativeElement.style.width = (this.bounds.width - clampedSecondaryWidth) + 'px'; 126 | this.primaryElement.nativeElement.style.minWidth = (this.bounds.width - clampedSecondaryWidth) + 'px'; 127 | 128 | // Set the secondary pane width. 129 | this.secondaryElement.nativeElement.style.width = clampedSecondaryWidth + 'px'; 130 | this.secondaryElement.nativeElement.style.minWidth = clampedSecondaryWidth + 'px'; 131 | 132 | // Place the resizer bar where it is expected. 133 | this.resizerElement.nativeElement.style.right = clampedSecondaryWidth + 'px'; 134 | this.resizerElement.nativeElement.style.top = this.bounds.top + 'px'; 135 | } 136 | 137 | /** 138 | * No event is fired when an individual element's size changes, so 139 | * the best we can do for now is reshape when window is resized. 140 | */ 141 | windowResized() { 142 | this.bounds = this.wrapperElement.nativeElement.getBoundingClientRect(); 143 | 144 | this.reshape(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/container.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {TNode} from './node'; 10 | import {RComment, RElement} from './renderer'; 11 | 12 | import {HOST, LView, NEXT, PARENT, T_HOST} from './view'; 13 | 14 | 15 | /** 16 | * Special location which allows easy identification of type. If we have an array which was 17 | * retrieved from the `LView` and that array has `true` at `TYPE` location, we know it is 18 | * `LContainer`. 19 | */ 20 | export const TYPE = 1; 21 | /** 22 | * Below are constants for LContainer indices to help us look up LContainer members 23 | * without having to remember the specific indices. 24 | * Uglify will inline these when minifying so there shouldn't be a cost. 25 | */ 26 | export const ACTIVE_INDEX = 2; 27 | 28 | // PARENT and NEXT are indices 3 and 4 29 | // As we already have these constants in LView, we don't need to re-create them. 30 | 31 | export const MOVED_VIEWS = 5; 32 | 33 | // T_HOST is index 6 34 | // We already have this constants in LView, we don't need to re-create it. 35 | 36 | export const NATIVE = 7; 37 | export const VIEW_REFS = 8; 38 | 39 | /** 40 | * Size of LContainer's header. Represents the index after which all views in the 41 | * container will be inserted. We need to keep a record of current views so we know 42 | * which views are already in the DOM (and don't need to be re-added) and so we can 43 | * remove views from the DOM when they are no longer required. 44 | */ 45 | export const CONTAINER_HEADER_OFFSET = 9; 46 | 47 | 48 | /** 49 | * Used to track: 50 | * - Inline embedded views (see: `ɵɵembeddedViewStart`) 51 | * - Transplanted `LView`s (see: `LView[DECLARATION_COMPONENT_VIEW])` 52 | */ 53 | export const enum ActiveIndexFlag { 54 | /** 55 | * Flag which signifies that the `LContainer` does not have any inline embedded views. 56 | */ 57 | DYNAMIC_EMBEDDED_VIEWS_ONLY = -1, 58 | 59 | /** 60 | * Flag to signify that this `LContainer` may have transplanted views which need to be change 61 | * detected. (see: `LView[DECLARATION_COMPONENT_VIEW])`. 62 | * 63 | * This flag once set is never unset for the `LContainer`. This means that when unset we can skip 64 | * a lot of work in `refreshDynamicEmbeddedViews`. But when set we still need to verify 65 | * that the `MOVED_VIEWS` are transplanted and on-push. 66 | */ 67 | HAS_TRANSPLANTED_VIEWS = 1, 68 | 69 | /** 70 | * Number of bits to shift inline embedded views counter to make space for other flags. 71 | */ 72 | SHIFT = 1, 73 | 74 | 75 | /** 76 | * When incrementing the active index for inline embedded views, the amount to increment to leave 77 | * space for other flags. 78 | */ 79 | INCREMENT = 1 << SHIFT, 80 | } 81 | 82 | /** 83 | * The state associated with a container. 84 | * 85 | * This is an array so that its structure is closer to LView. This helps 86 | * when traversing the view tree (which is a mix of containers and component 87 | * views), so we can jump to viewOrContainer[NEXT] in the same way regardless 88 | * of type. 89 | */ 90 | export interface LContainer extends Array { 91 | /** 92 | * The host element of this LContainer. 93 | * 94 | * The host could be an LView if this container is on a component node. 95 | * In that case, the component LView is its HOST. 96 | */ 97 | readonly[HOST]: RElement|RComment|LView; 98 | 99 | /** 100 | * This is a type field which allows us to differentiate `LContainer` from `StylingContext` in an 101 | * efficient way. The value is always set to `true` 102 | */ 103 | [TYPE]: true; 104 | 105 | /** 106 | * The next active index in the views array to read or write to. This helps us 107 | * keep track of where we are in the views array. 108 | * In the case the LContainer is created for a ViewContainerRef, 109 | * it is set to null to identify this scenario, as indices are "absolute" in that case, 110 | * i.e. provided directly by the user of the ViewContainerRef API. 111 | * 112 | * This is used by `ɵɵembeddedViewStart` to track which `LView` is currently active. 113 | * Because `ɵɵembeddedViewStart` is not generated by the compiler this feature is essentially 114 | * unused. 115 | * 116 | * The lowest bit signals that this `LContainer` has transplanted views which need to be change 117 | * detected as part of the declaration CD. (See `LView[DECLARATION_COMPONENT_VIEW]`) 118 | */ 119 | [ACTIVE_INDEX]: ActiveIndexFlag; 120 | 121 | /** 122 | * Access to the parent view is necessary so we can propagate back 123 | * up from inside a container to parent[NEXT]. 124 | */ 125 | [PARENT]: LView; 126 | 127 | /** 128 | * This allows us to jump from a container to a sibling container or component 129 | * view with the same parent, so we can remove listeners efficiently. 130 | */ 131 | [NEXT]: LView|LContainer|null; 132 | 133 | /** 134 | * A collection of views created based on the underlying `` element but inserted into 135 | * a different `LContainer`. We need to track views created from a given declaration point since 136 | * queries collect matches from the embedded view declaration point and _not_ the insertion point. 137 | */ 138 | [MOVED_VIEWS]: LView[]|null; 139 | 140 | /** 141 | * Pointer to the `TNode` which represents the host of the container. 142 | */ 143 | [T_HOST]: TNode; 144 | 145 | /** The comment element that serves as an anchor for this LContainer. */ 146 | readonly[NATIVE]: 147 | RComment; // TODO(misko): remove as this value can be gotten by unwrapping `[HOST]` 148 | 149 | /** 150 | * Array of `ViewRef`s used by any `ViewContainerRef`s that point to this container. 151 | * 152 | * This is lazily initialized by `ViewContainerRef` when the first view is inserted. 153 | */ 154 | [VIEW_REFS]: any[]|null; 155 | } 156 | 157 | // Note: This hack is necessary so we don't erroneously get a circular dependency 158 | // failure based on types. 159 | export const unusedValueExportToPlacateAjd = 1; 160 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/renderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** 10 | * The goal here is to make sure that the browser DOM API is the Renderer. 11 | * We do this by defining a subset of DOM API to be the renderer and then 12 | * use that at runtime for rendering. 13 | * 14 | * At runtime we can then use the DOM api directly, in server or web-worker 15 | * it will be easy to implement such API. 16 | */ 17 | 18 | import {getDocument} from './document'; 19 | 20 | // TODO: cleanup once the code is merged in angular/angular 21 | export enum RendererStyleFlags3 { 22 | Important = 1 << 0, 23 | DashCase = 1 << 1 24 | } 25 | 26 | export type Renderer3 = ObjectOrientedRenderer3 | ProceduralRenderer3; 27 | 28 | export type GlobalTargetName = 'document' | 'window' | 'body'; 29 | 30 | export type GlobalTargetResolver = (element: any) => { 31 | name: GlobalTargetName, target: EventTarget 32 | }; 33 | 34 | /** 35 | * Object Oriented style of API needed to create elements and text nodes. 36 | * 37 | * This is the native browser API style, e.g. operations are methods on individual objects 38 | * like HTMLElement. With this style, no additional code is needed as a facade 39 | * (reducing payload size). 40 | * */ 41 | export interface ObjectOrientedRenderer3 { 42 | createComment(data: string): RComment; 43 | createElement(tagName: string): RElement; 44 | createElementNS(namespace: string, tagName: string): RElement; 45 | createTextNode(data: string): RText; 46 | 47 | querySelector(selectors: string): RElement|null; 48 | } 49 | 50 | /** Returns whether the `renderer` is a `ProceduralRenderer3` */ 51 | export function isProceduralRenderer(renderer: ProceduralRenderer3 | ObjectOrientedRenderer3): 52 | renderer is ProceduralRenderer3 { 53 | return !!((renderer as any).listen); 54 | } 55 | 56 | /** 57 | * Procedural style of API needed to create elements and text nodes. 58 | * 59 | * In non-native browser environments (e.g. platforms such as web-workers), this is the 60 | * facade that enables element manipulation. This also facilitates backwards compatibility 61 | * with Renderer2. 62 | */ 63 | export interface ProceduralRenderer3 { 64 | destroy(): void; 65 | createComment(value: string): RComment; 66 | createElement(name: string, namespace?: string|null): RElement; 67 | createText(value: string): RText; 68 | /** 69 | * This property is allowed to be null / undefined, 70 | * in which case the view engine won't call it. 71 | * This is used as a performance optimization for production mode. 72 | */ 73 | destroyNode?: ((node: RNode) => void)|null; 74 | appendChild(parent: RElement, newChild: RNode): void; 75 | insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null): void; 76 | removeChild(parent: RElement, oldChild: RNode, isHostElement?: boolean): void; 77 | selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): RElement; 78 | 79 | parentNode(node: RNode): RElement|null; 80 | nextSibling(node: RNode): RNode|null; 81 | 82 | setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void; 83 | removeAttribute(el: RElement, name: string, namespace?: string|null): void; 84 | addClass(el: RElement, name: string): void; 85 | removeClass(el: RElement, name: string): void; 86 | setStyle( 87 | el: RElement, style: string, value: any, 88 | flags?: any | RendererStyleFlags3): void; 89 | removeStyle(el: RElement, style: string, flags?: any|RendererStyleFlags3): void; 90 | setProperty(el: RElement, name: string, value: any): void; 91 | setValue(node: RText|RComment, value: string): void; 92 | 93 | // TODO(misko): Deprecate in favor of addEventListener/removeEventListener 94 | listen( 95 | target: GlobalTargetName|RNode, eventName: string, 96 | callback: (event: any) => boolean | void): () => void; 97 | } 98 | 99 | export interface RendererFactory3 { 100 | createRenderer(hostElement: RElement|null, rendererType: any|null): Renderer3; 101 | begin?(): void; 102 | end?(): void; 103 | } 104 | 105 | export const domRendererFactory3: RendererFactory3 = { 106 | createRenderer: (hostElement: RElement | null, rendererType: any | null): 107 | Renderer3 => { return getDocument();} 108 | }; 109 | 110 | /** Subset of API needed for appending elements and text nodes. */ 111 | export interface RNode { 112 | /** 113 | * Returns the parent Element, Document, or DocumentFragment 114 | */ 115 | parentNode: RNode|null; 116 | 117 | 118 | /** 119 | * Returns the parent Element if there is one 120 | */ 121 | parentElement: RElement|null; 122 | 123 | /** 124 | * Gets the Node immediately following this one in the parent's childNodes 125 | */ 126 | nextSibling: RNode|null; 127 | 128 | /** 129 | * Removes a child from the current node and returns the removed node 130 | * @param oldChild the child node to remove 131 | */ 132 | removeChild(oldChild: RNode): RNode; 133 | 134 | /** 135 | * Insert a child node. 136 | * 137 | * Used exclusively for adding View root nodes into ViewAnchor location. 138 | */ 139 | insertBefore(newChild: RNode, refChild: RNode|null, isViewRoot: boolean): void; 140 | 141 | /** 142 | * Append a child node. 143 | * 144 | * Used exclusively for building up DOM which are static (ie not View roots) 145 | */ 146 | appendChild(newChild: RNode): RNode; 147 | } 148 | 149 | /** 150 | * Subset of API needed for writing attributes, properties, and setting up 151 | * listeners on Element. 152 | */ 153 | export interface RElement extends RNode { 154 | style: RCssStyleDeclaration; 155 | classList: RDomTokenList; 156 | className: string; 157 | textContent: string|null; 158 | setAttribute(name: string, value: string): void; 159 | removeAttribute(name: string): void; 160 | setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void; 161 | addEventListener(type: string, listener: EventListener, useCapture?: boolean): void; 162 | removeEventListener(type: string, listener?: EventListener, options?: boolean): void; 163 | 164 | setProperty?(name: string, value: any): void; 165 | } 166 | 167 | export interface RCssStyleDeclaration { 168 | removeProperty(propertyName: string): string; 169 | setProperty(propertyName: string, value: string|null, priority?: string): void; 170 | } 171 | 172 | export interface RDomTokenList { 173 | add(token: string): void; 174 | remove(token: string): void; 175 | } 176 | 177 | export interface RText extends RNode { textContent: string|null; } 178 | 179 | export interface RComment extends RNode { textContent: string|null; } 180 | 181 | // Note: This hack is necessary so we don't erroneously get a circular dependency 182 | // failure based on types. 183 | export const unusedValueExportToPlacateAjd = 1; 184 | -------------------------------------------------------------------------------- /packages/devtools-page/src/app/containers/component-tree/tree-diagram/tree-diagram.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | ElementRef, EventEmitter, 5 | Input, 6 | NgZone, 7 | OnInit, 8 | Output, 9 | ViewChild 10 | } from '@angular/core'; 11 | import * as d3 from 'd3'; 12 | import { ViewService } from "../../../core/view.service"; 13 | 14 | export interface SerializedTreeViewItem { 15 | id: string; 16 | name: string; 17 | tagName: string; 18 | children: SerializedTreeViewItem[]; 19 | parent: string; 20 | onPush: boolean; 21 | } 22 | 23 | @Component({ 24 | selector: 'tree-diagram', 25 | templateUrl: './tree-diagram.component.html', 26 | styleUrls: ['./tree-diagram.component.scss'], 27 | changeDetection: ChangeDetectionStrategy.OnPush 28 | }) 29 | export class TreeDiagramComponent implements OnInit { 30 | @ViewChild('svgContainer', { static: true }) private svg: ElementRef; 31 | @Input() 32 | get treeData() { 33 | return this._treeData; 34 | } 35 | set treeData(tree: SerializedTreeViewItem ) { 36 | if(!tree) return ; 37 | if (!this.gLink || !this.gNode) { 38 | this.init(); 39 | } 40 | this._treeData = tree; 41 | this.root = d3.hierarchy(tree); 42 | this.root.descendants().forEach((d, i) => { 43 | d.id = d.data.id; 44 | d.index = i; 45 | d._children = d.children; 46 | if (this.preSelected?.index === i ) { 47 | this.zone.run(() => this.viewService.selectNode(d)); 48 | } 49 | }); 50 | this.render(this.root); 51 | }; 52 | @Output() hoverNode = new EventEmitter(); 53 | 54 | _treeData; gLink; gNode; root; preSelected; rectW = 150; rectH = 30; rect; 55 | 56 | constructor(private viewService: ViewService, private zone: NgZone) { } 57 | 58 | ngOnInit(): void { 59 | if (!this.gLink || !this.gNode) { 60 | this.init(); 61 | } 62 | } 63 | 64 | init() { 65 | const svg = d3.select(this.svg.nativeElement); 66 | this.gLink = svg.append("g") 67 | .attr("fill", "none") 68 | .attr("stroke", "#555") 69 | .attr("stroke-opacity", 0.6) 70 | .attr("stroke-width", 1.5); 71 | 72 | this.gNode = svg.append("g") 73 | .attr("user-select", "none"); 74 | } 75 | 76 | render(source) { 77 | const margin = { 78 | top: 20, 79 | right: 100 + this.rectW, 80 | bottom: 200, 81 | left: 100 82 | }; 83 | const tree = d3.tree().nodeSize([this.rectW + 10, this.rectH + 100]); 84 | const svg = d3.select(this.svg.nativeElement); 85 | const diagonal = d3.linkVertical().x(d => d.xl).y(d => d.yl); 86 | 87 | const duration = d3.event && d3.event.altKey ? 2500 : 250; 88 | const nodes = this.root.descendants().reverse(); 89 | const links = this.root.links(); 90 | 91 | // Compute the new tree layout. 92 | tree(this.root); 93 | 94 | let left = this.root; 95 | let right = this.root; 96 | let top = this.root; 97 | let bottom = this.root; 98 | this.root.eachBefore(node => { 99 | if (node.x < left.x) left = node; 100 | if (node.x > right.x) right = node; 101 | if (node.y < top.y) top = node; 102 | if (node.y > bottom.y) bottom = node; 103 | }); 104 | 105 | const width = right.x - left.x + margin.right + margin.left; 106 | const height = bottom.y - top.y + margin.top + margin.bottom; 107 | this.root.x0 = 0; 108 | this.root.y0 = height/2; 109 | 110 | const transition = svg.transition() 111 | .duration(duration) 112 | .attr("viewBox", [left.x - margin.left, - margin.top, width, height]) 113 | .tween("resize", () => () => svg.dispatch("toggle")); 114 | 115 | // Update the nodes… 116 | const node = this.gNode.selectAll("g") 117 | .data(nodes, d => d.data.id); 118 | 119 | // Enter any new nodes at the parent's previous position. 120 | const nodeEnter = node.enter().append("g") 121 | .attr("transform", d => `translate(${source.x0},${source.y0})`) 122 | .attr("fill-opacity", 0) 123 | .attr("stroke-opacity", 0) 124 | .style("cursor", "pointer") 125 | .on("mouseover", (d) => this.hoverNode.emit(d.data.id)) 126 | .on("mouseout", () => this.hoverNode.emit()) 127 | .on("click", this.clickNode); 128 | 129 | this.rect = nodeEnter.append("rect") 130 | .attr('id', (d) => 'r' + d.data.id) 131 | .attr("width", this.rectW) 132 | .attr("height", this.rectH) 133 | .attr("stroke", "black") 134 | .attr("stroke-width", 1) 135 | .style("fill", (d) => d._children ? "lightsteelblue" : "#fff"); 136 | 137 | nodeEnter.append("text") 138 | .attr("x", this.rectW / 2) 139 | .attr("y", this.rectH / 2) 140 | .attr("dy", ".35em") 141 | .attr("text-anchor", "middle") 142 | .text((d) => d.data.name); 143 | 144 | // Transition nodes to their new position. 145 | node.merge(nodeEnter).transition(transition) 146 | .attr("transform", d => `translate(${d.x},${d.y})`) 147 | .attr("fill-opacity", 1) 148 | .attr("stroke-opacity", 1); 149 | 150 | // Transition exiting nodes to the parent's new position. 151 | node.exit().transition(transition).remove() 152 | .attr("transform", d => `translate(${source.x},${source.y})`) 153 | .attr("fill-opacity", 0) 154 | .attr("stroke-opacity", 0); 155 | 156 | // Update the links… 157 | const link = this.gLink.selectAll("path") 158 | .data(links, d => d.target.id); 159 | 160 | // Enter any new links at the parent's previous position. 161 | const linkEnter = link.enter().append("path") 162 | .attr('id', (d) => 'l' + d.target.data.id) 163 | .attr("x", this.rectW / 2) 164 | .attr("y", this.rectH / 2); 165 | 166 | // Transition links to their new position. 167 | link.merge(linkEnter).transition(transition) 168 | .duration(duration) 169 | .attr("d", d => { 170 | const source = { 171 | ...d.source, 172 | xl: d.source.x + this.rectW / 2, 173 | yl: d.source.y + this.rectH 174 | }; 175 | const target = { 176 | ...d.target, 177 | xl: d.target.x + this.rectW / 2, 178 | yl: d.target.y 179 | }; 180 | return diagonal({source, target}); 181 | }); 182 | 183 | // Transition exiting nodes to the parent's new position. 184 | link.exit().remove() 185 | .attr("l", () => { 186 | const o = {x: source.x, y: source.y}; 187 | return diagonal({source: o, target: o}); 188 | }); 189 | 190 | // Stash the old positions for transition. 191 | this.root.eachBefore(d => { 192 | d.x0 = d.x; 193 | d.y0 = d.y; 194 | }); 195 | } 196 | 197 | clickNode = (d) => { 198 | d.children = d.children ? null : d._children; 199 | this.render(d); 200 | this.zone.run(() => this.viewService.selectNode(d)); 201 | this.preSelected?.selected?.style("fill", (d) => d._children ? "lightsteelblue" : "#fff"); 202 | const selected = d3.select(`#r${d.id}`); 203 | selected.style('fill', () => 'lightgray'); 204 | this.preSelected = { 205 | index: d.index, 206 | selected 207 | }; 208 | }; 209 | 210 | zoomIn() { 211 | const currentHeight = this.svg.nativeElement.clientHeight; 212 | this.svg.nativeElement.style.height = `${currentHeight * 2}px`; 213 | } 214 | 215 | zoomOut() { 216 | const currentHeight = this.svg.nativeElement.clientHeight; 217 | this.svg.nativeElement.style.height = `${currentHeight / 2}px`; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /packages/core/util/treeView.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHILD_HEAD, 3 | CONTEXT, 4 | FLAGS, 5 | HOST, 6 | LView, 7 | LViewFlags, 8 | NEXT, 9 | PARENT, 10 | RENDERER_FACTORY, 11 | TVIEW 12 | } from "../angular/interfaces/view"; 13 | import { RenderFlags } from "../angular/interfaces/definition"; 14 | import { scheduleOutsideOfZone } from "./zone"; 15 | import { CanvasFactory } from "./canvas"; 16 | import { ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer } from "../angular/interfaces/container"; 17 | import { isLContainer } from "../angular/interfaces/type_checks"; 18 | import { getComponentLViewByIndex } from "../angular/util/view_utils"; 19 | import { createMessage } from "../../communication/messager"; 20 | import { MessageMethod, MessageType } from "../../communication/message.type"; 21 | import { generate } from "shortid"; 22 | import { NG_PROFILER_ID } from "../constants"; 23 | import { findLView } from "../angular/render3/context_discovery"; 24 | 25 | declare const ng; 26 | 27 | export interface TreeViewItem { 28 | lView: LView; 29 | children: TreeViewItem[]; 30 | parent?: TreeViewItem; 31 | } 32 | 33 | export interface SerializedTreeViewItem { 34 | id: string; 35 | name: string; 36 | tagName: string; 37 | children: SerializedTreeViewItem[]; 38 | parent: string; 39 | onPush: boolean; 40 | context: any; 41 | } 42 | 43 | export interface ViewProfile { 44 | id: string; 45 | start: number; 46 | end: number; 47 | } 48 | 49 | class TreeView { 50 | serialisedTreeView: SerializedTreeViewItem; 51 | treeLViewMap = new Map(); 52 | enabled: boolean; 53 | private _highlightView: boolean; 54 | private _highlightTree: boolean; 55 | private _bodyLView: LView; 56 | 57 | get highlightView() { 58 | return this._highlightView; 59 | } 60 | 61 | set highlightView(status: boolean) { 62 | this._highlightView = status; 63 | } 64 | 65 | get highlightTree() { 66 | return this._highlightTree; 67 | } 68 | 69 | set highlightTree(status: boolean) { 70 | this._highlightTree = status; 71 | } 72 | 73 | get bodyLView() { 74 | if (!this._bodyLView) { 75 | this.bodyLView = findLView(document.body); 76 | } 77 | return this._bodyLView; 78 | }; 79 | 80 | set bodyLView(view: LView) { 81 | this._bodyLView = view; 82 | } 83 | 84 | enable = () => this.enabled = true; 85 | disable = () => this.enabled = false; 86 | 87 | setView = (treeView: TreeViewItem) => { 88 | this.serialisedTreeView = this.serialiseTreeViewItem(treeView.children[0]); 89 | console.log(treeView, this.serialisedTreeView); 90 | }; 91 | 92 | serialiseTreeViewItem( 93 | treeViewItem: TreeViewItem 94 | ): SerializedTreeViewItem { 95 | if (!treeViewItem) return ; 96 | return { 97 | id: treeViewItem.lView[HOST][NG_PROFILER_ID], 98 | name: this.getComponentName(treeViewItem.lView), 99 | tagName: treeViewItem.lView && treeViewItem.lView[HOST]!?.localName, 100 | children: treeViewItem.children && treeViewItem.children.map(loopTreeViewItem => 101 | this.serialiseTreeViewItem(loopTreeViewItem) 102 | ), 103 | parent: this.getComponentName(treeViewItem.lView[PARENT]), 104 | context: this.getContextState(treeViewItem.lView[CONTEXT]), 105 | onPush: treeViewItem.lView && (treeViewItem.lView[FLAGS] & LViewFlags.CheckAlways) === 0 106 | }; 107 | } 108 | 109 | getComponentName = (lView: LView | LContainer) => { 110 | return lView[CONTEXT].constructor.name.length > 1 ? lView[CONTEXT].constructor.name : lView[HOST] && lView[HOST]['localName']; 111 | }; 112 | 113 | getContextState = (context) => { 114 | return Object.keys(context).reduce((acc, key) => { 115 | if (typeof context[key] !== 'object' && typeof context[key] !== 'function' 116 | // TODO: handle object 117 | // || context[key] && Object.getPrototypeOf(context[key]).isPrototypeOf(Object) 118 | ) { 119 | acc[key] = context[key]; 120 | } 121 | return acc; 122 | }, {}); 123 | }; 124 | 125 | applyChanges = (data: SerializedTreeViewItem) => { 126 | const lView = this.treeLViewMap.get(data.id); 127 | Object.keys(data.context).forEach(key => { 128 | lView[CONTEXT][key] = data.context[key]; 129 | }); 130 | ng.applyChanges(lView[CONTEXT]); 131 | }; 132 | 133 | attachChildComponents = (hostLView: LView, components: number[], addChildElement: (children: TreeViewItem[] | TreeViewItem) => void) => { 134 | const children = []; 135 | const addElement = (treeViewItem) => { 136 | children.push(treeViewItem); 137 | }; 138 | 139 | for (let i = 0; i < components.length; i++) { 140 | const componentView = getComponentLViewByIndex(components[i], hostLView); 141 | this.attachComponent(componentView, addElement); 142 | } 143 | addChildElement(children); 144 | }; 145 | 146 | attachComponent = (componentView: LView, addElement: (treeView: TreeViewItem) => void) => { 147 | const componentTView = componentView[TVIEW]; 148 | const treeView = { 149 | lView: componentView, 150 | children: [] 151 | }; 152 | const addChildElement = (childElements: TreeViewItem[]) => { 153 | treeView.children = treeView.children.concat(...childElements);; 154 | }; 155 | this.attachTemplate(componentView); 156 | const childComponents = componentTView.components; 157 | if (childComponents !== null) { 158 | this.attachChildComponents(componentView, childComponents, addChildElement); 159 | } 160 | this.attachDynamicEmbeddedViews(componentView, addChildElement); 161 | addElement(treeView); 162 | }; 163 | 164 | /** 165 | * Goes over dynamic embedded views (ones created through ViewContainerRef APIs) and refreshes 166 | * them by executing an associated template function. 167 | */ 168 | attachDynamicEmbeddedViews = (lView: LView, addChildElement) => { 169 | let children = []; 170 | let viewOrContainer = lView[CHILD_HEAD]; 171 | while (viewOrContainer !== null) { 172 | // Note: viewOrContainer can be an LView or an LContainer instance, but here we are only 173 | // interested in LContainer 174 | let activeIndexFlag: ActiveIndexFlag; 175 | if (isLContainer(viewOrContainer) && 176 | (activeIndexFlag = viewOrContainer[ACTIVE_INDEX]) >> ActiveIndexFlag.SHIFT === 177 | ActiveIndexFlag.DYNAMIC_EMBEDDED_VIEWS_ONLY) { 178 | 179 | for (let i = CONTAINER_HEADER_OFFSET; i < viewOrContainer.length; i++) { 180 | const embeddedLView = viewOrContainer[i] as LView; 181 | const embeddedTView = embeddedLView?.[TVIEW]; 182 | 183 | embeddedTView?.components && this.attachComponent(embeddedLView, (element) => { 184 | children = [...children, ...element.children] 185 | }); 186 | } 187 | if ((activeIndexFlag & ActiveIndexFlag.HAS_TRANSPLANTED_VIEWS) !== 0) { 188 | // We should only CD moved views if the component where they were inserted does not match 189 | // the component where they were declared and insertion is on-push. Moved views also 190 | // contains intra component moves, or check-always which need to be skipped. 191 | // refreshTransplantedViews(viewOrContainer, lView[DECLARATION_COMPONENT_VIEW] !); 192 | } 193 | } 194 | viewOrContainer = viewOrContainer[NEXT]; 195 | } 196 | addChildElement(children); 197 | }; 198 | 199 | private attachTemplate = (lView: LView) => { 200 | const tView = lView[TVIEW]; 201 | let uuid; 202 | if (lView[HOST] && !lView[HOST][NG_PROFILER_ID]) { 203 | uuid = generate(); 204 | lView[HOST][NG_PROFILER_ID] = uuid; 205 | this.treeLViewMap.set(uuid, lView); 206 | this.profilingTemplate(lView, (start, end) => { 207 | scheduleOutsideOfZone(() => { 208 | try { 209 | console.log(start); 210 | postMessage(createMessage(MessageType.VIEW_PROFILES, MessageMethod.Response, {id: uuid, start, end}), '*'); 211 | } catch (e) { 212 | console.log(e); 213 | } 214 | }) 215 | // console.log(lView, start, end, end - start); 216 | }); 217 | } 218 | 219 | const originTemplate = tView.template; 220 | if (!originTemplate) return ; 221 | 222 | tView.template = (...args) => { 223 | originTemplate(args[0], args[1]); 224 | if (!this.enabled) return ; 225 | if (args[0] === RenderFlags.Update && lView[HOST]) { 226 | scheduleOutsideOfZone(() => { 227 | try { 228 | this.highlightView && CanvasFactory.draw(uuid, this.treeLViewMap.get(uuid)[HOST]!?.getBoundingClientRect()); 229 | this.highlightTree && postMessage(createMessage(MessageType.UPDATE_TREE, MessageMethod.Response, uuid), '*'); 230 | } catch (e) { 231 | console.log(e); 232 | } 233 | }) 234 | } 235 | } 236 | }; 237 | 238 | private profilingTemplate(componentView: LView, record: (start: number, end: number) => void) { 239 | const renderer = componentView[RENDERER_FACTORY]; 240 | const oriBegin = renderer.begin; 241 | const oriEnd = renderer.end; 242 | 243 | let start, end; 244 | renderer.begin = function() { 245 | oriBegin.apply(this); 246 | start = performance.now(); 247 | }; 248 | renderer.end = function () { 249 | oriEnd.apply(this); 250 | end = performance.now(); 251 | record(start, end); 252 | }; 253 | } 254 | } 255 | export const TreeViewFactory = new TreeView(); -------------------------------------------------------------------------------- /packages/core/angular/interfaces/definition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {TAttributes, TConstants} from './node'; 10 | import {CssSelectorList} from './projection'; 11 | import {TView} from './view'; 12 | 13 | 14 | /** 15 | * Definition of what a template rendering function should look like for a component. 16 | */ 17 | export type ComponentTemplate = { 18 | // Note: the ctx parameter is typed as T|U, as using only U would prevent a template with 19 | // e.g. ctx: {} from being assigned to ComponentTemplate as TypeScript won't infer U = any 20 | // in that scenario. By including T this incompatibility is resolved. 21 | (rf: RenderFlags, ctx: T | U): void; 22 | }; 23 | 24 | /** 25 | * Definition of what a view queries function should look like. 26 | */ 27 | export type ViewQueriesFunction = (rf: RenderFlags, ctx: U) => void; 28 | 29 | /** 30 | * Definition of what a content queries function should look like. 31 | */ 32 | export type ContentQueriesFunction = 33 | (rf: RenderFlags, ctx: U, directiveIndex: number) => void; 34 | 35 | /** 36 | * Definition of what a factory function should look like. 37 | */ 38 | export type FactoryFn = { 39 | /** 40 | * Subclasses without an explicit constructor call through to the factory of their base 41 | * definition, providing it with their own constructor to instantiate. 42 | */ 43 | (t: any): U; 44 | 45 | /** 46 | * If no constructor to instantiate is provided, an instance of type T itself is created. 47 | */ 48 | (t?: undefined): T; 49 | }; 50 | 51 | /** 52 | * Flags passed into template functions to determine which blocks (i.e. creation, update) 53 | * should be executed. 54 | * 55 | * Typically, a template runs both the creation block and the update block on initialization and 56 | * subsequent runs only execute the update block. However, dynamically created views require that 57 | * the creation block be executed separately from the update block (for backwards compat). 58 | */ 59 | export const enum RenderFlags { 60 | /* Whether to run the creation block (e.g. create elements and directives) */ 61 | Create = 0b01, 62 | 63 | /* Whether to run the update block (e.g. refresh bindings) */ 64 | Update = 0b10 65 | } 66 | 67 | /** 68 | * A subclass of `Type` which has a static `ɵcmp`:`ComponentDef` field making it 69 | * consumable for rendering. 70 | */ 71 | export interface ComponentType { ɵcmp: never; } 72 | 73 | /** 74 | * A subclass of `Type` which has a static `ɵdir`:`DirectiveDef` field making it 75 | * consumable for rendering. 76 | */ 77 | export interface DirectiveType { 78 | ɵdir: never; 79 | ɵfac: () => T; 80 | } 81 | 82 | /** 83 | * A subclass of `Type` which has a static `ɵpipe`:`PipeDef` field making it 84 | * consumable for rendering. 85 | */ 86 | export interface PipeType { ɵpipe: never; } 87 | 88 | /** 89 | * @codeGenApi 90 | */ 91 | export type ɵɵDirectiveDefWithMeta< 92 | T, Selector extends string, ExportAs extends string[], InputMap extends{[key: string]: string}, 93 | OutputMap extends{[key: string]: string}, QueryFields extends string[]> = DirectiveDef; 94 | 95 | /** 96 | * Runtime link information for Directives. 97 | * 98 | * This is an internal data structure used by the render to link 99 | * directives into templates. 100 | * 101 | * NOTE: Always use `defineDirective` function to create this object, 102 | * never create the object directly since the shape of this object 103 | * can change between versions. 104 | * 105 | * @param Selector type metadata specifying the selector of the directive or component 106 | * 107 | * See: {@link defineDirective} 108 | */ 109 | export interface DirectiveDef { 110 | /** 111 | * A dictionary mapping the inputs' minified property names to their public API names, which 112 | * are their aliases if any, or their original unminified property names 113 | * (as in `@Input('alias') propertyName: any;`). 114 | */ 115 | readonly inputs: {[P in keyof T]: string}; 116 | 117 | /** 118 | * @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of 119 | * public or minified name. 120 | */ 121 | readonly declaredInputs: {[P in keyof T]: string}; 122 | 123 | /** 124 | * A dictionary mapping the outputs' minified property names to their public API names, which 125 | * are their aliases if any, or their original unminified property names 126 | * (as in `@Output('alias') propertyName: any;`). 127 | */ 128 | readonly outputs: {[P in keyof T]: string}; 129 | 130 | /** 131 | * Function to create and refresh content queries associated with a given directive. 132 | */ 133 | contentQueries: ContentQueriesFunction|null; 134 | 135 | /** 136 | * Query-related instructions for a directive. Note that while directives don't have a 137 | * view and as such view queries won't necessarily do anything, there might be 138 | * components that extend the directive. 139 | */ 140 | viewQuery: ViewQueriesFunction|null; 141 | 142 | /** 143 | * Refreshes host bindings on the associated directive. 144 | */ 145 | readonly hostBindings: HostBindingsFunction|null; 146 | 147 | /** 148 | * The number of bindings in this directive `hostBindings` (including pure fn bindings). 149 | * 150 | * Used to calculate the length of the component's LView array, so we 151 | * can pre-fill the array and set the host binding start index. 152 | */ 153 | readonly hostVars: number; 154 | 155 | /** 156 | * Assign static attribute values to a host element. 157 | * 158 | * This property will assign static attribute values as well as class and style 159 | * values to a host element. Since attribute values can consist of different types of values, the 160 | * `hostAttrs` array must include the values in the following format: 161 | * 162 | * attrs = [ 163 | * // static attributes (like `title`, `name`, `id`...) 164 | * attr1, value1, attr2, value, 165 | * 166 | * // a single namespace value (like `x:id`) 167 | * NAMESPACE_MARKER, namespaceUri1, name1, value1, 168 | * 169 | * // another single namespace value (like `x:name`) 170 | * NAMESPACE_MARKER, namespaceUri2, name2, value2, 171 | * 172 | * // a series of CSS classes that will be applied to the element (no spaces) 173 | * CLASSES_MARKER, class1, class2, class3, 174 | * 175 | * // a series of CSS styles (property + value) that will be applied to the element 176 | * STYLES_MARKER, prop1, value1, prop2, value2 177 | * ] 178 | * 179 | * All non-class and non-style attributes must be defined at the start of the list 180 | * first before all class and style values are set. When there is a change in value 181 | * type (like when classes and styles are introduced) a marker must be used to separate 182 | * the entries. The marker values themselves are set via entries found in the 183 | * [AttributeMarker] enum. 184 | */ 185 | readonly hostAttrs: TAttributes|null; 186 | 187 | /** Token representing the directive. Used by DI. */ 188 | readonly type: any; 189 | 190 | /** Function that resolves providers and publishes them into the DI system. */ 191 | providersResolver: 192 | ((def: DirectiveDef, processProvidersFn?) => 193 | void)|null; 194 | 195 | /** The selectors that will be used to match nodes to this directive. */ 196 | readonly selectors: CssSelectorList; 197 | 198 | /** 199 | * Name under which the directive is exported (for use with local references in template) 200 | */ 201 | readonly exportAs: string[]|null; 202 | 203 | /** 204 | * Factory function used to create a new directive instance. Will be null initially. 205 | * Populated when the factory is first requested by directive instantiation logic. 206 | */ 207 | readonly factory: FactoryFn|null; 208 | 209 | /* The following are lifecycle hooks for this component */ 210 | readonly onChanges: (() => void)|null; 211 | readonly onInit: (() => void)|null; 212 | readonly doCheck: (() => void)|null; 213 | readonly afterContentInit: (() => void)|null; 214 | readonly afterContentChecked: (() => void)|null; 215 | readonly afterViewInit: (() => void)|null; 216 | readonly afterViewChecked: (() => void)|null; 217 | readonly onDestroy: (() => void)|null; 218 | 219 | /** 220 | * The features applied to this directive 221 | */ 222 | readonly features: DirectiveDefFeature[]|null; 223 | 224 | setInput: 225 | (( 226 | this: DirectiveDef, instance: U, value: any, publicName: string, 227 | privateName: string) => void)|null; 228 | } 229 | 230 | /** 231 | * @codeGenApi 232 | */ 233 | export type ɵɵComponentDefWithMeta< 234 | T, Selector extends String, ExportAs extends string[], InputMap extends{[key: string]: string}, 235 | OutputMap extends{[key: string]: string}, QueryFields extends string[]> = ComponentDef; 236 | 237 | /** 238 | * @codeGenApi 239 | */ 240 | export type ɵɵFactoryDef = () => T; 241 | 242 | /** 243 | * Runtime link information for Components. 244 | * 245 | * This is an internal data structure used by the render to link 246 | * components into templates. 247 | * 248 | * NOTE: Always use `defineComponent` function to create this object, 249 | * never create the object directly since the shape of this object 250 | * can change between versions. 251 | * 252 | * See: {@link defineComponent} 253 | */ 254 | export interface ComponentDef extends DirectiveDef { 255 | /** 256 | * Runtime unique component ID. 257 | */ 258 | readonly id: string; 259 | 260 | /** 261 | * The View template of the component. 262 | */ 263 | readonly template: ComponentTemplate; 264 | 265 | /** Constants associated with the component's view. */ 266 | readonly consts: TConstants|null; 267 | 268 | /** 269 | * An array of `ngContent[selector]` values that were found in the template. 270 | */ 271 | readonly ngContentSelectors?: string[]; 272 | 273 | /** 274 | * A set of styles that the component needs to be present for component to render correctly. 275 | */ 276 | readonly styles: string[]; 277 | 278 | /** 279 | * The number of nodes, local refs, and pipes in this component template. 280 | * 281 | * Used to calculate the length of the component's LView array, so we 282 | * can pre-fill the array and set the binding start index. 283 | */ 284 | // TODO(kara): remove queries from this count 285 | readonly decls: number; 286 | 287 | /** 288 | * The number of bindings in this component template (including pure fn bindings). 289 | * 290 | * Used to calculate the length of the component's LView array, so we 291 | * can pre-fill the array and set the host binding start index. 292 | */ 293 | readonly vars: number; 294 | 295 | /** 296 | * Query-related instructions for a component. 297 | */ 298 | viewQuery: ViewQueriesFunction|null; 299 | 300 | /** 301 | * The view encapsulation type, which determines how styles are applied to 302 | * DOM elements. One of 303 | * - `Emulated` (default): Emulate native scoping of styles. 304 | * - `Native`: Use the native encapsulation mechanism of the renderer. 305 | * - `ShadowDom`: Use modern [ShadowDOM](https://w3c.github.io/webcomponents/spec/shadow/) and 306 | * create a ShadowRoot for component's host element. 307 | * - `None`: Do not provide any template or style encapsulation. 308 | */ 309 | readonly encapsulation: any; 310 | 311 | /** 312 | * Defines arbitrary developer-defined data to be stored on a renderer instance. 313 | * This is useful for renderers that delegate to other renderers. 314 | */ 315 | readonly data: {[kind: string]: any}; 316 | 317 | /** Whether or not this component's ChangeDetectionStrategy is OnPush */ 318 | readonly onPush: boolean; 319 | 320 | /** 321 | * Registry of directives and components that may be found in this view. 322 | * 323 | * The property is either an array of `DirectiveDef`s or a function which returns the array of 324 | * `DirectiveDef`s. The function is necessary to be able to support forward declarations. 325 | */ 326 | directiveDefs: DirectiveDefListOrFactory|null; 327 | 328 | /** 329 | * Registry of pipes that may be found in this view. 330 | * 331 | * The property is either an array of `PipeDefs`s or a function which returns the array of 332 | * `PipeDefs`s. The function is necessary to be able to support forward declarations. 333 | */ 334 | pipeDefs: PipeDefListOrFactory|null; 335 | 336 | /** 337 | * The set of schemas that declare elements to be allowed in the component's template. 338 | */ 339 | schemas: any[]|null; 340 | 341 | /** 342 | * Ivy runtime uses this place to store the computed tView for the component. This gets filled on 343 | * the first run of component. 344 | */ 345 | tView: TView|null; 346 | 347 | /** 348 | * Used to store the result of `noSideEffects` function so that it is not removed by closure 349 | * compiler. The property should never be read. 350 | */ 351 | readonly _?: never; 352 | } 353 | 354 | /** 355 | * Runtime link information for Pipes. 356 | * 357 | * This is an internal data structure used by the renderer to link 358 | * pipes into templates. 359 | * 360 | * NOTE: Always use `definePipe` function to create this object, 361 | * never create the object directly since the shape of this object 362 | * can change between versions. 363 | * 364 | * See: {@link definePipe} 365 | */ 366 | export interface PipeDef { 367 | /** Token representing the pipe. */ 368 | type: any; 369 | 370 | /** 371 | * Pipe name. 372 | * 373 | * Used to resolve pipe in templates. 374 | */ 375 | readonly name: string; 376 | 377 | /** 378 | * Factory function used to create a new pipe instance. Will be null initially. 379 | * Populated when the factory is first requested by pipe instantiation logic. 380 | */ 381 | factory: FactoryFn|null; 382 | 383 | /** 384 | * Whether or not the pipe is pure. 385 | * 386 | * Pure pipes result only depends on the pipe input and not on internal 387 | * state of the pipe. 388 | */ 389 | readonly pure: boolean; 390 | 391 | /* The following are lifecycle hooks for this pipe */ 392 | onDestroy: (() => void)|null; 393 | } 394 | 395 | /** 396 | * @codeGenApi 397 | */ 398 | export type ɵɵPipeDefWithMeta = PipeDef; 399 | 400 | export interface DirectiveDefFeature { 401 | (directiveDef: DirectiveDef): void; 402 | /** 403 | * Marks a feature as something that {@link InheritDefinitionFeature} will execute 404 | * during inheritance. 405 | * 406 | * NOTE: DO NOT SET IN ROOT OF MODULE! Doing so will result in tree-shakers/bundlers 407 | * identifying the change as a side effect, and the feature will be included in 408 | * every bundle. 409 | */ 410 | ngInherit?: true; 411 | } 412 | 413 | export interface ComponentDefFeature { 414 | (componentDef: ComponentDef): void; 415 | /** 416 | * Marks a feature as something that {@link InheritDefinitionFeature} will execute 417 | * during inheritance. 418 | * 419 | * NOTE: DO NOT SET IN ROOT OF MODULE! Doing so will result in tree-shakers/bundlers 420 | * identifying the change as a side effect, and the feature will be included in 421 | * every bundle. 422 | */ 423 | ngInherit?: true; 424 | } 425 | 426 | 427 | /** 428 | * Type used for directiveDefs on component definition. 429 | * 430 | * The function is necessary to be able to support forward declarations. 431 | */ 432 | export type DirectiveDefListOrFactory = (() => DirectiveDefList) | DirectiveDefList; 433 | 434 | export type DirectiveDefList = (DirectiveDef| ComponentDef)[]; 435 | 436 | export type DirectiveTypesOrFactory = (() => DirectiveTypeList) | DirectiveTypeList; 437 | 438 | export type DirectiveTypeList = 439 | (DirectiveType| ComponentType| 440 | any/* Type as workaround for: Microsoft/TypeScript/issues/4881 */)[]; 441 | 442 | export type HostBindingsFunction = (rf: RenderFlags, ctx: U) => void; 443 | 444 | /** 445 | * Type used for PipeDefs on component definition. 446 | * 447 | * The function is necessary to be able to support forward declarations. 448 | */ 449 | export type PipeDefListOrFactory = (() => PipeDefList) | PipeDefList; 450 | 451 | export type PipeDefList = PipeDef[]; 452 | 453 | export type PipeTypesOrFactory = (() => PipeTypeList) | PipeTypeList; 454 | 455 | export type PipeTypeList = 456 | (PipeType| any/* Type as workaround for: Microsoft/TypeScript/issues/4881 */)[]; 457 | 458 | 459 | // Note: This hack is necessary so we don't erroneously get a circular dependency 460 | // failure based on types. 461 | export const unusedValueExportToPlacateAjd = 1; 462 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/node.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import {CssSelector} from './projection'; 9 | import {RNode} from './renderer'; 10 | import {LView, TView} from './view'; 11 | 12 | 13 | 14 | /** 15 | * TNodeType corresponds to the {@link TNode} `type` property. 16 | */ 17 | export const enum TNodeType { 18 | /** 19 | * The TNode contains information about an {@link LContainer} for embedded views. 20 | */ 21 | Container = 0, 22 | /** 23 | * The TNode contains information about an `` projection 24 | */ 25 | Projection = 1, 26 | /** 27 | * The TNode contains information about an {@link LView} 28 | */ 29 | View = 2, 30 | /** 31 | * The TNode contains information about a DOM element aka {@link RNode}. 32 | */ 33 | Element = 3, 34 | /** 35 | * The TNode contains information about an `` element {@link RNode}. 36 | */ 37 | ElementContainer = 4, 38 | /** 39 | * The TNode contains information about an ICU comment used in `i18n`. 40 | */ 41 | IcuContainer = 5, 42 | } 43 | 44 | /** 45 | * Corresponds to the TNode.flags property. 46 | */ 47 | export const enum TNodeFlags { 48 | /** Bit #1 - This bit is set if the node is a host for any directive (including a component) */ 49 | isDirectiveHost = 0x1, 50 | 51 | /** 52 | * Bit #2 - This bit is set if the node is a host for a component. 53 | * 54 | * Setting this bit implies that the `isDirectiveHost` bit is set as well. 55 | * */ 56 | isComponentHost = 0x2, 57 | 58 | /** Bit #3 - This bit is set if the node has been projected */ 59 | isProjected = 0x4, 60 | 61 | /** Bit #4 - This bit is set if any directive on this node has content queries */ 62 | hasContentQuery = 0x8, 63 | 64 | /** Bit #5 - This bit is set if the node has any "class" inputs */ 65 | hasClassInput = 0x10, 66 | 67 | /** Bit #6 - This bit is set if the node has any "style" inputs */ 68 | hasStyleInput = 0x20, 69 | 70 | /** Bit #7 This bit is set if the node has been detached by i18n */ 71 | isDetached = 0x40, 72 | 73 | /** 74 | * Bit #8 - This bit is set if the node has directives with host bindings. 75 | * 76 | * This flags allows us to guard host-binding logic and invoke it only on nodes 77 | * that actually have directives with host bindings. 78 | */ 79 | hasHostBindings = 0x80, 80 | } 81 | 82 | /** 83 | * Corresponds to the TNode.providerIndexes property. 84 | */ 85 | export const enum TNodeProviderIndexes { 86 | /** The index of the first provider on this node is encoded on the least significant bits */ 87 | ProvidersStartIndexMask = 0b00000000000000001111111111111111, 88 | 89 | /** The count of view providers from the component on this node is encoded on the 16 most 90 | significant bits */ 91 | CptViewProvidersCountShift = 16, 92 | CptViewProvidersCountShifter = 0b00000000000000010000000000000000, 93 | } 94 | /** 95 | * A set of marker values to be used in the attributes arrays. These markers indicate that some 96 | * items are not regular attributes and the processing should be adapted accordingly. 97 | */ 98 | export const enum AttributeMarker { 99 | /** 100 | * An implicit marker which indicates that the value in the array are of `attributeKey`, 101 | * `attributeValue` format. 102 | * 103 | * NOTE: This is implicit as it is the type when no marker is present in array. We indicate that 104 | * it should not be present at runtime by the negative number. 105 | */ 106 | ImplicitAttributes = -1, 107 | 108 | /** 109 | * Marker indicates that the following 3 values in the attributes array are: 110 | * namespaceUri, attributeName, attributeValue 111 | * in that order. 112 | */ 113 | NamespaceURI = 0, 114 | 115 | /** 116 | * Signals class declaration. 117 | * 118 | * Each value following `Classes` designates a class name to include on the element. 119 | * ## Example: 120 | * 121 | * Given: 122 | * ``` 123 | *
... 124 | * ``` 125 | * 126 | * the generated code is: 127 | * ``` 128 | * var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz']; 129 | * ``` 130 | */ 131 | Classes = 1, 132 | 133 | /** 134 | * Signals style declaration. 135 | * 136 | * Each pair of values following `Styles` designates a style name and value to include on the 137 | * element. 138 | * ## Example: 139 | * 140 | * Given: 141 | * ``` 142 | *
...
143 | * ``` 144 | * 145 | * the generated code is: 146 | * ``` 147 | * var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red']; 148 | * ``` 149 | */ 150 | Styles = 2, 151 | 152 | /** 153 | * Signals that the following attribute names were extracted from input or output bindings. 154 | * 155 | * For example, given the following HTML: 156 | * 157 | * ``` 158 | *
159 | * ``` 160 | * 161 | * the generated code is: 162 | * 163 | * ``` 164 | * var _c1 = ['moo', 'car', AttributeMarker.Bindings, 'foo', 'bar']; 165 | * ``` 166 | */ 167 | Bindings = 3, 168 | 169 | /** 170 | * Signals that the following attribute names were hoisted from an inline-template declaration. 171 | * 172 | * For example, given the following HTML: 173 | * 174 | * ``` 175 | *
176 | * ``` 177 | * 178 | * the generated code for the `template()` instruction would include: 179 | * 180 | * ``` 181 | * ['dirA', '', AttributeMarker.Bindings, 'dirB', AttributeMarker.Template, 'ngFor', 'ngForOf', 182 | * 'ngForTrackBy', 'let-value'] 183 | * ``` 184 | * 185 | * while the generated code for the `element()` instruction inside the template function would 186 | * include: 187 | * 188 | * ``` 189 | * ['dirA', '', AttributeMarker.Bindings, 'dirB'] 190 | * ``` 191 | */ 192 | Template = 4, 193 | 194 | /** 195 | * Signals that the following attribute is `ngProjectAs` and its value is a parsed `CssSelector`. 196 | * 197 | * For example, given the following HTML: 198 | * 199 | * ``` 200 | *

201 | * ``` 202 | * 203 | * the generated code for the `element()` instruction would include: 204 | * 205 | * ``` 206 | * ['attr', 'value', AttributeMarker.ProjectAs, ['', 'title', '']] 207 | * ``` 208 | */ 209 | ProjectAs = 5, 210 | 211 | /** 212 | * Signals that the following attribute will be translated by runtime i18n 213 | * 214 | * For example, given the following HTML: 215 | * 216 | * ``` 217 | *
218 | * ``` 219 | * 220 | * the generated code is: 221 | * 222 | * ``` 223 | * var _c1 = ['moo', 'car', AttributeMarker.I18n, 'foo', 'bar']; 224 | */ 225 | I18n = 6, 226 | } 227 | 228 | /** 229 | * A combination of: 230 | * - Attribute names and values. 231 | * - Special markers acting as flags to alter attributes processing. 232 | * - Parsed ngProjectAs selectors. 233 | */ 234 | export type TAttributes = (string | AttributeMarker | CssSelector)[]; 235 | 236 | /** 237 | * Constants that are associated with a view. Includes: 238 | * - Attribute arrays. 239 | * - Local definition arrays. 240 | */ 241 | export type TConstants = (TAttributes | string)[]; 242 | 243 | /** 244 | * Binding data (flyweight) for a particular node that is shared between all templates 245 | * of a specific type. 246 | * 247 | * If a property is: 248 | * - PropertyAliases: that property's data was generated and this is it 249 | * - Null: that property's data was already generated and nothing was found. 250 | * - Undefined: that property's data has not yet been generated 251 | * 252 | * see: https://en.wikipedia.org/wiki/Flyweight_pattern for more on the Flyweight pattern 253 | */ 254 | export interface TNode { 255 | /** The type of the TNode. See TNodeType. */ 256 | type: TNodeType; 257 | 258 | /** 259 | * Index of the TNode in TView.data and corresponding native element in LView. 260 | * 261 | * This is necessary to get from any TNode to its corresponding native element when 262 | * traversing the node tree. 263 | * 264 | * If index is -1, this is a dynamically created container node or embedded view node. 265 | */ 266 | index: number; 267 | 268 | /** 269 | * The index of the closest injector in this node's LView. 270 | * 271 | * If the index === -1, there is no injector on this node or any ancestor node in this view. 272 | * 273 | * If the index !== -1, it is the index of this node's injector OR the index of a parent injector 274 | * in the same view. We pass the parent injector index down the node tree of a view so it's 275 | * possible to find the parent injector without walking a potentially deep node tree. Injector 276 | * indices are not set across view boundaries because there could be multiple component hosts. 277 | * 278 | * If tNode.injectorIndex === tNode.parent.injectorIndex, then the index belongs to a parent 279 | * injector. 280 | */ 281 | injectorIndex: number; 282 | 283 | /** 284 | * Stores starting index of the directives. 285 | */ 286 | directiveStart: number; 287 | 288 | /** 289 | * Stores final exclusive index of the directives. 290 | */ 291 | directiveEnd: number; 292 | 293 | /** 294 | * Stores the last directive which had a styling instruction. 295 | * 296 | * Initial value of this is `-1` which means that no `hostBindings` styling instruction has 297 | * executed. As `hostBindings` instructions execute they set the value to the index of the 298 | * `DirectiveDef` which contained the last `hostBindings` styling instruction. 299 | * 300 | * Valid values are: 301 | * - `-1` No `hostBindings` instruction has executed. 302 | * - `directiveStart <= directiveStylingLast < directiveEnd`: Points to the `DirectiveDef` of the 303 | * last styling instruction which executed in the `hostBindings`. 304 | * 305 | * This data is needed so that styling instructions know which static styling data needs to be 306 | * collected from the `DirectiveDef.hostAttrs`. A styling instruction needs to collect all data 307 | * since last styling instruction. 308 | */ 309 | directiveStylingLast: number; 310 | 311 | /** 312 | * Stores indexes of property bindings. This field is only set in the ngDevMode and holds indexes 313 | * of property bindings so TestBed can get bound property metadata for a given node. 314 | */ 315 | propertyBindings: number[]|null; 316 | 317 | /** 318 | * Stores if Node isComponent, isProjected, hasContentQuery, hasClassInput and hasStyleInput etc. 319 | */ 320 | flags: TNodeFlags; 321 | 322 | /** 323 | * This number stores two values using its bits: 324 | * 325 | * - the index of the first provider on that node (first 16 bits) 326 | * - the count of view providers from the component on this node (last 16 bits) 327 | */ 328 | // TODO(misko): break this into actual vars. 329 | providerIndexes: TNodeProviderIndexes; 330 | 331 | /** The tag name associated with this node. */ 332 | tagName: string|null; 333 | 334 | /** 335 | * Attributes associated with an element. We need to store attributes to support various use-cases 336 | * (attribute injection, content projection with selectors, directives matching). 337 | * Attributes are stored statically because reading them from the DOM would be way too slow for 338 | * content projection and queries. 339 | * 340 | * Since attrs will always be calculated first, they will never need to be marked undefined by 341 | * other instructions. 342 | * 343 | * For regular attributes a name of an attribute and its value alternate in the array. 344 | * e.g. ['role', 'checkbox'] 345 | * This array can contain flags that will indicate "special attributes" (attributes with 346 | * namespaces, attributes extracted from bindings and outputs). 347 | */ 348 | attrs: TAttributes|null; 349 | 350 | /** 351 | * Same as `TNode.attrs` but contains merged data across all directive host bindings. 352 | * 353 | * We need to keep `attrs` as unmerged so that it can be used for attribute selectors. 354 | * We merge attrs here so that it can be used in a performant way for initial rendering. 355 | * 356 | * The `attrs` are merged in first pass in following order: 357 | * - Component's `hostAttrs` 358 | * - Directives' `hostAttrs` 359 | * - Template `TNode.attrs` associated with the current `TNode`. 360 | */ 361 | mergedAttrs: TAttributes|null; 362 | 363 | /** 364 | * A set of local names under which a given element is exported in a template and 365 | * visible to queries. An entry in this array can be created for different reasons: 366 | * - an element itself is referenced, ex.: `
` 367 | * - a component is referenced, ex.: `` 368 | * - a directive is referenced, ex.: ``. 369 | * 370 | * A given element might have different local names and those names can be associated 371 | * with a directive. We store local names at even indexes while odd indexes are reserved 372 | * for directive index in a view (or `-1` if there is no associated directive). 373 | * 374 | * Some examples: 375 | * - `
` => `["foo", -1]` 376 | * - `` => `["foo", myCmptIdx]` 377 | * - `` => `["foo", myCmptIdx, "bar", directiveIdx]` 378 | * - `
` => `["foo", -1, "bar", directiveIdx]` 379 | */ 380 | localNames: (string|number)[]|null; 381 | 382 | /** Information about input properties that need to be set once from attribute data. */ 383 | initialInputs: InitialInputData|null|undefined; 384 | 385 | /** 386 | * Input data for all directives on this node. `null` means that there are no directives with 387 | * inputs on this node. 388 | */ 389 | inputs: PropertyAliases|null; 390 | 391 | /** 392 | * Output data for all directives on this node. `null` means that there are no directives with 393 | * outputs on this node. 394 | */ 395 | outputs: PropertyAliases|null; 396 | 397 | /** 398 | * The TView or TViews attached to this node. 399 | * 400 | * If this TNode corresponds to an LContainer with inline views, the container will 401 | * need to store separate static data for each of its view blocks (TView[]). Otherwise, 402 | * nodes in inline views with the same index as nodes in their parent views will overwrite 403 | * each other, as they are in the same template. 404 | * 405 | * Each index in this array corresponds to the static data for a certain 406 | * view. So if you had V(0) and V(1) in a container, you might have: 407 | * 408 | * [ 409 | * [{tagName: 'div', attrs: ...}, null], // V(0) TView 410 | * [{tagName: 'button', attrs ...}, null] // V(1) TView 411 | * 412 | * If this TNode corresponds to an LContainer with a template (e.g. structural 413 | * directive), the template's TView will be stored here. 414 | * 415 | * If this TNode corresponds to an element, tViews will be null . 416 | */ 417 | tViews: TView|TView[]|null; 418 | 419 | /** 420 | * The next sibling node. Necessary so we can propagate through the root nodes of a view 421 | * to insert them or remove them from the DOM. 422 | */ 423 | next: TNode|null; 424 | 425 | /** 426 | * The next projected sibling. Since in Angular content projection works on the node-by-node basis 427 | * the act of projecting nodes might change nodes relationship at the insertion point (target 428 | * view). At the same time we need to keep initial relationship between nodes as expressed in 429 | * content view. 430 | */ 431 | projectionNext: TNode|null; 432 | 433 | /** 434 | * First child of the current node. 435 | * 436 | * For component nodes, the child will always be a ContentChild (in same view). 437 | * For embedded view nodes, the child will be in their child view. 438 | */ 439 | child: TNode|null; 440 | 441 | /** 442 | * Parent node (in the same view only). 443 | * 444 | * We need a reference to a node's parent so we can append the node to its parent's native 445 | * element at the appropriate time. 446 | * 447 | * If the parent would be in a different view (e.g. component host), this property will be null. 448 | * It's important that we don't try to cross component boundaries when retrieving the parent 449 | * because the parent will change (e.g. index, attrs) depending on where the component was 450 | * used (and thus shouldn't be stored on TNode). In these cases, we retrieve the parent through 451 | * LView.node instead (which will be instance-specific). 452 | * 453 | * If this is an inline view node (V), the parent will be its container. 454 | */ 455 | parent: TElementNode|TContainerNode|null; 456 | 457 | /** 458 | * List of projected TNodes for a given component host element OR index into the said nodes. 459 | * 460 | * For easier discussion assume this example: 461 | * ``'s view definition: 462 | * ``` 463 | * content1 464 | * content2 465 | * ``` 466 | * ``'s view definition: 467 | * ``` 468 | * 469 | * ``` 470 | * 471 | * If `Array.isArray(projection)` then `TNode` is a host element: 472 | * - `projection` stores the content nodes which are to be projected. 473 | * - The nodes represent categories defined by the selector: For example: 474 | * `` would represent the heads for `` 475 | * and `` respectively. 476 | * - The nodes we store in `projection` are heads only, we used `.next` to get their 477 | * siblings. 478 | * - The nodes `.next` is sorted/rewritten as part of the projection setup. 479 | * - `projection` size is equal to the number of projections ``. The size of 480 | * `c1` will be `1` because `` has only one ``. 481 | * - we store `projection` with the host (`c1`, `c2`) rather than the `` (`cont1`) 482 | * because the same component (``) can be used in multiple locations (`c1`, `c2`) and as 483 | * a result have different set of nodes to project. 484 | * - without `projection` it would be difficult to efficiently traverse nodes to be projected. 485 | * 486 | * If `typeof projection == 'number'` then `TNode` is a `` element: 487 | * - `projection` is an index of the host's `projection`Nodes. 488 | * - This would return the first head node to project: 489 | * `getHost(currentTNode).projection[currentTNode.projection]`. 490 | * - When projecting nodes the parent node retrieved may be a `` node, in which case 491 | * the process is recursive in nature. 492 | * 493 | * If `projection` is of type `RNode[][]` than we have a collection of native nodes passed as 494 | * projectable nodes during dynamic component creation. 495 | */ 496 | projection: (TNode|RNode[])[]|number|null; 497 | 498 | /** 499 | * A collection of all style static values for an element. 500 | * 501 | * This field will be populated if and when: 502 | * 503 | * - There are one or more initial styles on an element (e.g. `
`) 504 | */ 505 | styles: string|null; 506 | 507 | /** 508 | * A `KeyValueArray` version of residual `styles`. 509 | * 510 | * When there are styling instructions than each instruction stores the static styling 511 | * which is of lower priority than itself. This means that there may be a higher priority styling 512 | * than the instruction. 513 | * 514 | * Imagine: 515 | * ``` 516 | *
517 | * 518 | * @Directive({ 519 | * host: { 520 | * style: 'color: lowest; ', 521 | * '[styles.color]': 'exp' // ɵɵstyleProp('color', ctx.exp); 522 | * } 523 | * }) 524 | * ``` 525 | * 526 | * In the above case: 527 | * - `color: lowest` is stored with `ɵɵstyleProp('color', ctx.exp);` instruction 528 | * - `color: highest` is the residual and is stored here. 529 | * 530 | * - `undefined': not initialized. 531 | * - `null`: initialized but `styles` is `null` 532 | * - `KeyValueArray`: parsed version of `styles`. 533 | */ 534 | residualStyles: any|undefined|null; 535 | 536 | /** 537 | * A collection of all class static values for an element. 538 | * 539 | * This field will be populated if and when: 540 | * 541 | * - There are one or more initial classes on an element (e.g. `
`) 542 | */ 543 | classes: string|null; 544 | 545 | /** 546 | * A `KeyValueArray` version of residual `classes`. 547 | * 548 | * Same as `TNode.residualStyles` but for classes. 549 | * 550 | * - `undefined': not initialized. 551 | * - `null`: initialized but `classes` is `null` 552 | * - `KeyValueArray`: parsed version of `classes`. 553 | */ 554 | residualClasses: any|undefined|null; 555 | 556 | /** 557 | * Stores the head/tail index of the class bindings. 558 | * 559 | * - If no bindings, the head and tail will both be 0. 560 | * - If there are template bindings, stores the head/tail of the class bindings in the template. 561 | * - If no template bindings but there are host bindings, the head value will point to the last 562 | * host binding for "class" (not the head of the linked list), tail will be 0. 563 | * 564 | * See: `style_binding_list.ts` for details. 565 | * 566 | * This is used by `insertTStylingBinding` to know where the next styling binding should be 567 | * inserted so that they can be sorted in priority order. 568 | */ 569 | classBindings: any; 570 | 571 | /** 572 | * Stores the head/tail index of the class bindings. 573 | * 574 | * - If no bindings, the head and tail will both be 0. 575 | * - If there are template bindings, stores the head/tail of the style bindings in the template. 576 | * - If no template bindings but there are host bindings, the head value will point to the last 577 | * host binding for "style" (not the head of the linked list), tail will be 0. 578 | * 579 | * See: `style_binding_list.ts` for details. 580 | * 581 | * This is used by `insertTStylingBinding` to know where the next styling binding should be 582 | * inserted so that they can be sorted in priority order. 583 | */ 584 | styleBindings: any; 585 | } 586 | 587 | /** Static data for an element */ 588 | export interface TElementNode extends TNode { 589 | /** Index in the data[] array */ 590 | index: number; 591 | child: TElementNode|TTextNode|TElementContainerNode|TContainerNode|TProjectionNode|null; 592 | /** 593 | * Element nodes will have parents unless they are the first node of a component or 594 | * embedded view (which means their parent is in a different view and must be 595 | * retrieved using viewData[HOST_NODE]). 596 | */ 597 | parent: TElementNode|TElementContainerNode|null; 598 | tViews: null; 599 | 600 | /** 601 | * If this is a component TNode with projection, this will be an array of projected 602 | * TNodes or native nodes (see TNode.projection for more info). If it's a regular element node or 603 | * a component without projection, it will be null. 604 | */ 605 | projection: (TNode|RNode[])[]|null; 606 | } 607 | 608 | /** Static data for a text node */ 609 | export interface TTextNode extends TNode { 610 | /** Index in the data[] array */ 611 | index: number; 612 | child: null; 613 | /** 614 | * Text nodes will have parents unless they are the first node of a component or 615 | * embedded view (which means their parent is in a different view and must be 616 | * retrieved using LView.node). 617 | */ 618 | parent: TElementNode|TElementContainerNode|null; 619 | tViews: null; 620 | projection: null; 621 | } 622 | 623 | /** Static data for an LContainer */ 624 | export interface TContainerNode extends TNode { 625 | /** 626 | * Index in the data[] array. 627 | * 628 | * If it's -1, this is a dynamically created container node that isn't stored in 629 | * data[] (e.g. when you inject ViewContainerRef) . 630 | */ 631 | index: number; 632 | child: null; 633 | 634 | /** 635 | * Container nodes will have parents unless: 636 | * 637 | * - They are the first node of a component or embedded view 638 | * - They are dynamically created 639 | */ 640 | parent: TElementNode|TElementContainerNode|null; 641 | tViews: TView|TView[]|null; 642 | projection: null; 643 | } 644 | 645 | /** Static data for an */ 646 | export interface TElementContainerNode extends TNode { 647 | /** Index in the LView[] array. */ 648 | index: number; 649 | child: TElementNode|TTextNode|TContainerNode|TElementContainerNode|TProjectionNode|null; 650 | parent: TElementNode|TElementContainerNode|null; 651 | tViews: null; 652 | projection: null; 653 | } 654 | 655 | /** Static data for an ICU expression */ 656 | export interface TIcuContainerNode extends TNode { 657 | /** Index in the LView[] array. */ 658 | index: number; 659 | child: TElementNode|TTextNode|null; 660 | parent: TElementNode|TElementContainerNode|null; 661 | tViews: null; 662 | projection: null; 663 | /** 664 | * Indicates the current active case for an ICU expression. 665 | * It is null when there is no active case. 666 | */ 667 | activeCaseIndex: number|null; 668 | } 669 | 670 | /** Static data for a view */ 671 | export interface TViewNode extends TNode { 672 | /** If -1, it's a dynamically created view. Otherwise, it is the view block ID. */ 673 | index: number; 674 | child: TElementNode|TTextNode|TElementContainerNode|TContainerNode|TProjectionNode|null; 675 | parent: TContainerNode|null; 676 | tViews: null; 677 | projection: null; 678 | } 679 | 680 | /** Static data for an LProjectionNode */ 681 | export interface TProjectionNode extends TNode { 682 | /** Index in the data[] array */ 683 | child: null; 684 | /** 685 | * Projection nodes will have parents unless they are the first node of a component 686 | * or embedded view (which means their parent is in a different view and must be 687 | * retrieved using LView.node). 688 | */ 689 | parent: TElementNode|TElementContainerNode|null; 690 | tViews: null; 691 | 692 | /** Index of the projection node. (See TNode.projection for more info.) */ 693 | projection: number; 694 | } 695 | 696 | /** 697 | * A union type representing all TNode types that can host a directive. 698 | */ 699 | export type TDirectiveHostNode = TElementNode | TContainerNode | TElementContainerNode; 700 | 701 | /** 702 | * This mapping is necessary so we can set input properties and output listeners 703 | * properly at runtime when property names are minified or aliased. 704 | * 705 | * Key: unminified / public input or output name 706 | * Value: array containing minified / internal name and related directive index 707 | * 708 | * The value must be an array to support inputs and outputs with the same name 709 | * on the same node. 710 | */ 711 | export type PropertyAliases = { 712 | // This uses an object map because using the Map type would be too slow 713 | [key: string]: PropertyAliasValue 714 | }; 715 | 716 | /** 717 | * Store the runtime input or output names for all the directives. 718 | * 719 | * i+0: directive instance index 720 | * i+1: privateName 721 | * 722 | * e.g. [0, 'change-minified'] 723 | */ 724 | export type PropertyAliasValue = (number | string)[]; 725 | 726 | /** 727 | * This array contains information about input properties that 728 | * need to be set once from attribute data. It's ordered by 729 | * directive index (relative to element) so it's simple to 730 | * look up a specific directive's initial input data. 731 | * 732 | * Within each sub-array: 733 | * 734 | * i+0: attribute name 735 | * i+1: minified/internal input name 736 | * i+2: initial value 737 | * 738 | * If a directive on a node does not have any input properties 739 | * that should be set from attributes, its index is set to null 740 | * to avoid a sparse array. 741 | * 742 | * e.g. [null, ['role-min', 'minified-input', 'button']] 743 | */ 744 | export type InitialInputData = (InitialInputs | null)[]; 745 | 746 | /** 747 | * Used by InitialInputData to store input properties 748 | * that should be set once from attributes. 749 | * 750 | * i+0: attribute name 751 | * i+1: minified/internal input name 752 | * i+2: initial value 753 | * 754 | * e.g. ['role-min', 'minified-input', 'button'] 755 | */ 756 | export type InitialInputs = string[]; 757 | 758 | // Note: This hack is necessary so we don't erroneously get a circular dependency 759 | // failure based on types. 760 | export const unusedValueExportToPlacateAjd = 1; 761 | 762 | /** 763 | * Type representing a set of TNodes that can have local refs (`#foo`) placed on them. 764 | */ 765 | export type TNodeWithLocalRefs = TContainerNode | TElementNode | TElementContainerNode; 766 | 767 | /** 768 | * Type for a function that extracts a value for a local refs. 769 | * Example: 770 | * - `
` - `nativeDivEl` should point to the native `
` element; 771 | * - `` - `tplRef` should point to the `TemplateRef` instance; 772 | */ 773 | export type LocalRefExtractor = (tNode: TNodeWithLocalRefs, currentView: LView) => any; 774 | 775 | /** 776 | * Returns `true` if the `TNode` has a directive which has `@Input()` for `class` binding. 777 | * 778 | * ``` 779 | *
780 | * ``` 781 | * and 782 | * ``` 783 | * @Directive({ 784 | * }) 785 | * class MyDirective { 786 | * @Input() 787 | * class: string; 788 | * } 789 | * ``` 790 | * 791 | * In the above case it is necessary to write the reconciled styling information into the 792 | * directive's input. 793 | * 794 | * @param tNode 795 | */ 796 | export function hasClassInput(tNode: TNode) { 797 | return (tNode.flags & TNodeFlags.hasClassInput) !== 0; 798 | } 799 | 800 | /** 801 | * Returns `true` if the `TNode` has a directive which has `@Input()` for `style` binding. 802 | * 803 | * ``` 804 | *
805 | * ``` 806 | * and 807 | * ``` 808 | * @Directive({ 809 | * }) 810 | * class MyDirective { 811 | * @Input() 812 | * class: string; 813 | * } 814 | * ``` 815 | * 816 | * In the above case it is necessary to write the reconciled styling information into the 817 | * directive's input. 818 | * 819 | * @param tNode 820 | */ 821 | export function hasStyleInput(tNode: TNode) { 822 | return (tNode.flags & TNodeFlags.hasStyleInput) !== 0; 823 | } 824 | -------------------------------------------------------------------------------- /packages/core/angular/interfaces/view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | 10 | import {LContainer} from './container'; 11 | import {TConstants, TElementNode, TNode, TViewNode} from './node'; 12 | import {PlayerHandler} from './player'; 13 | import {RElement, Renderer3, RendererFactory3} from './renderer'; 14 | import { 15 | ComponentDef, 16 | ComponentTemplate, 17 | DirectiveDef, 18 | DirectiveDefList, 19 | PipeDef, 20 | PipeDefList, 21 | ViewQueriesFunction 22 | } from "./definition"; 23 | import { InjectionToken, SchemaMetadata, Type } from "@angular/core"; 24 | 25 | 26 | 27 | // Below are constants for LView indices to help us look up LView members 28 | // without having to remember the specific indices. 29 | // Uglify will inline these when minifying so there shouldn't be a cost. 30 | export const HOST = 0; 31 | export const TVIEW = 1; 32 | export const FLAGS = 2; 33 | export const PARENT = 3; 34 | export const NEXT = 4; 35 | export const QUERIES = 5; 36 | export const T_HOST = 6; 37 | export const CLEANUP = 7; 38 | export const CONTEXT = 8; 39 | export const INJECTOR = 9; 40 | export const RENDERER_FACTORY = 10; 41 | export const RENDERER = 11; 42 | export const SANITIZER = 12; 43 | export const CHILD_HEAD = 13; 44 | export const CHILD_TAIL = 14; 45 | export const DECLARATION_VIEW = 15; 46 | export const DECLARATION_COMPONENT_VIEW = 16; 47 | export const DECLARATION_LCONTAINER = 17; 48 | export const PREORDER_HOOK_FLAGS = 18; 49 | /** Size of LView's header. Necessary to adjust for it when setting slots. */ 50 | export const HEADER_OFFSET = 19; 51 | 52 | 53 | // This interface replaces the real LView interface if it is an arg or a 54 | // return value of a public instruction. This ensures we don't need to expose 55 | // the actual interface, which should be kept private. 56 | export interface OpaqueViewState { 57 | '__brand__': 'Brand for OpaqueViewState that nothing will match'; 58 | } 59 | 60 | 61 | /** 62 | * `LView` stores all of the information needed to process the instructions as 63 | * they are invoked from the template. Each embedded view and component view has its 64 | * own `LView`. When processing a particular view, we set the `viewData` to that 65 | * `LView`. When that view is done processing, the `viewData` is set back to 66 | * whatever the original `viewData` was before (the parent `LView`). 67 | * 68 | * Keeping separate state for each view facilities view insertion / deletion, so we 69 | * don't have to edit the data array based on which views are present. 70 | */ 71 | export interface LView extends Array { 72 | /** 73 | * The host node for this LView instance, if this is a component view. 74 | * If this is an embedded view, HOST will be null. 75 | */ 76 | [HOST]: Element|null; 77 | 78 | /** 79 | * The static data for this view. We need a reference to this so we can easily walk up the 80 | * node tree in DI and get the TView.data array associated with a node (where the 81 | * directive defs are stored). 82 | */ 83 | readonly[TVIEW]: TView; 84 | 85 | /** Flags for this view. See LViewFlags for more info. */ 86 | [FLAGS]: LViewFlags; 87 | 88 | /** 89 | * This may store an {@link LView} or {@link LContainer}. 90 | * 91 | * `LView` - The parent view. This is needed when we exit the view and must restore the previous 92 | * LView. Without this, the render method would have to keep a stack of 93 | * views as it is recursively rendering templates. 94 | * 95 | * `LContainer` - The current view is part of a container, and is an embedded view. 96 | */ 97 | [PARENT]: LView|LContainer|null; 98 | 99 | /** 100 | * 101 | * The next sibling LView or LContainer. 102 | * 103 | * Allows us to propagate between sibling view states that aren't in the same 104 | * container. Embedded views already have a node.next, but it is only set for 105 | * views in the same container. We need a way to link component views and views 106 | * across containers as well. 107 | */ 108 | [NEXT]: LView|LContainer|null; 109 | 110 | /** Queries active for this view - nodes from a view are reported to those queries. */ 111 | [QUERIES]: any|null; 112 | 113 | /** 114 | * Pointer to the `TViewNode` or `TElementNode` which represents the root of the view. 115 | * 116 | * If `TViewNode`, this is an embedded view of a container. We need this to be able to 117 | * efficiently find the `LViewNode` when inserting the view into an anchor. 118 | * 119 | * If `TElementNode`, this is the LView of a component. 120 | * 121 | * If null, this is the root view of an application (root component is in this view). 122 | */ 123 | [T_HOST]: TViewNode|TElementNode|null; 124 | 125 | /** 126 | * When a view is destroyed, listeners need to be released and outputs need to be 127 | * unsubscribed. This context array stores both listener functions wrapped with 128 | * their context and output subscription instances for a particular view. 129 | * 130 | * These change per LView instance, so they cannot be stored on TView. Instead, 131 | * TView.cleanup saves an index to the necessary context in this array. 132 | */ 133 | // TODO: flatten into LView[] 134 | [CLEANUP]: any[]|null; 135 | 136 | /** 137 | * - For dynamic views, this is the context with which to render the template (e.g. 138 | * `NgForContext`), or `{}` if not defined explicitly. 139 | * - For root view of the root component the context contains change detection data. 140 | * - For non-root components, the context is the component instance, 141 | * - For inline views, the context is null. 142 | */ 143 | [CONTEXT]: {}|RootContext|null; 144 | 145 | /** An optional Module Injector to be used as fall back after Element Injectors are consulted. */ 146 | readonly[INJECTOR]: any|null; 147 | 148 | /** Factory to be used for creating Renderer. */ 149 | [RENDERER_FACTORY]: RendererFactory3; 150 | 151 | /** Renderer to be used for this view. */ 152 | [RENDERER]: Renderer3; 153 | 154 | /** An optional custom sanitizer. */ 155 | [SANITIZER]: any|null; 156 | 157 | /** 158 | * Reference to the first LView or LContainer beneath this LView in 159 | * the hierarchy. 160 | * 161 | * Necessary to store this so views can traverse through their nested views 162 | * to remove listeners and call onDestroy callbacks. 163 | */ 164 | [CHILD_HEAD]: LView|LContainer|null; 165 | 166 | /** 167 | * The last LView or LContainer beneath this LView in the hierarchy. 168 | * 169 | * The tail allows us to quickly add a new state to the end of the view list 170 | * without having to propagate starting from the first child. 171 | */ 172 | [CHILD_TAIL]: LView|LContainer|null; 173 | 174 | /** 175 | * View where this view's template was declared. 176 | * 177 | * Only applicable for dynamically created views. Will be null for inline/component views. 178 | * 179 | * The template for a dynamically created view may be declared in a different view than 180 | * it is inserted. We already track the "insertion view" (view where the template was 181 | * inserted) in LView[PARENT], but we also need access to the "declaration view" 182 | * (view where the template was declared). Otherwise, we wouldn't be able to call the 183 | * view's template function with the proper contexts. Context should be inherited from 184 | * the declaration view tree, not the insertion view tree. 185 | * 186 | * Example (AppComponent template): 187 | * 188 | * <-- declared here --> 189 | * <-- inserted inside this component --> 190 | * 191 | * The above is declared in the AppComponent template, but it will be passed into 192 | * SomeComp and inserted there. In this case, the declaration view would be the AppComponent, 193 | * but the insertion view would be SomeComp. When we are removing views, we would want to 194 | * traverse through the insertion view to clean up listeners. When we are calling the 195 | * template function during change detection, we need the declaration view to get inherited 196 | * context. 197 | */ 198 | [DECLARATION_VIEW]: LView|null; 199 | 200 | 201 | /** 202 | * Points to the declaration component view, used to track transplanted `LView`s. 203 | * 204 | * See: `DECLARATION_VIEW` which points to the actual `LView` where it was declared, whereas 205 | * `DECLARATION_COMPONENT_VIEW` points to the component which may not be same as 206 | * `DECLARATION_VIEW`. 207 | * 208 | * Example: 209 | * ``` 210 | * <#VIEW #myComp> 211 | *
212 | * ... 213 | *
214 | * 215 | * ``` 216 | * In the above case `DECLARATION_VIEW` for `myTmpl` points to the `LView` of `ngIf` whereas 217 | * `DECLARATION_COMPONENT_VIEW` points to `LView` of the `myComp` which owns the template. 218 | * 219 | * The reason for this is that all embedded views are always check-always whereas the component 220 | * view can be check-always or on-push. When we have a transplanted view it is important to 221 | * determine if we have transplanted a view from check-always declaration to on-push insertion 222 | * point. In such a case the transplanted view needs to be added to the `LContainer` in the 223 | * declared `LView` and CD during the declared view CD (in addition to the CD at the insertion 224 | * point.) (Any transplanted views which are intra Component are of no interest because the CD 225 | * strategy of declaration and insertion will always be the same, because it is the same 226 | * component.) 227 | * 228 | * Queries already track moved views in `LView[DECLARATION_LCONTAINER]` and 229 | * `LContainer[MOVED_VIEWS]`. However the queries also track `LView`s which moved within the same 230 | * component `LView`. Transplanted views are a subset of moved views, and we use 231 | * `DECLARATION_COMPONENT_VIEW` to differentiate them. As in this example. 232 | * 233 | * Example showing intra component `LView` movement. 234 | * ``` 235 | * <#VIEW #myComp> 236 | *
237 | * Content to render when condition is true. 238 | * Content to render when condition is false. 239 | * 240 | * ``` 241 | * The `thenBlock` and `elseBlock` is moved but not transplanted. 242 | * 243 | * Example showing inter component `LView` movement (transplanted view). 244 | * ``` 245 | * <#VIEW #myComp> 246 | * ... 247 | * 248 | * 249 | * ``` 250 | * In the above example `myTmpl` is passed into a different component. If `insertion-component` 251 | * instantiates `myTmpl` and `insertion-component` is on-push then the `LContainer` needs to be 252 | * marked as containing transplanted views and those views need to be CD as part of the 253 | * declaration CD. 254 | * 255 | * 256 | * When change detection runs, it iterates over `[MOVED_VIEWS]` and CDs any child `LView`s where 257 | * the `DECLARATION_COMPONENT_VIEW` of the current component and the child `LView` does not match 258 | * (it has been transplanted across components.) 259 | * 260 | * Note: `[DECLARATION_COMPONENT_VIEW]` points to itself if the LView is a component view (the 261 | * simplest / most common case). 262 | * 263 | * see also: 264 | * - https://hackmd.io/@mhevery/rJUJsvv9H write up of the problem 265 | * - `LContainer[ACTIVE_INDEX]` for flag which marks which `LContainer` has transplanted views. 266 | * - `LContainer[TRANSPLANT_HEAD]` and `LContainer[TRANSPLANT_TAIL]` storage for transplanted 267 | * - `LView[DECLARATION_LCONTAINER]` similar problem for queries 268 | * - `LContainer[MOVED_VIEWS]` similar problem for queries 269 | */ 270 | [DECLARATION_COMPONENT_VIEW]: LView; 271 | 272 | /** 273 | * A declaration point of embedded views (ones instantiated based on the content of a 274 | * ), null for other types of views. 275 | * 276 | * We need to track all embedded views created from a given declaration point so we can prepare 277 | * query matches in a proper order (query matches are ordered based on their declaration point and 278 | * _not_ the insertion point). 279 | */ 280 | [DECLARATION_LCONTAINER]: LContainer|null; 281 | 282 | /** 283 | * More flags for this view. See PreOrderHookFlags for more info. 284 | */ 285 | [PREORDER_HOOK_FLAGS]: PreOrderHookFlags; 286 | } 287 | 288 | /** Flags associated with an LView (saved in LView[FLAGS]) */ 289 | export const enum LViewFlags { 290 | /** The state of the init phase on the first 2 bits */ 291 | InitPhaseStateIncrementer = 0b00000000001, 292 | InitPhaseStateMask = 0b00000000011, 293 | 294 | /** 295 | * Whether or not the view is in creationMode. 296 | * 297 | * This must be stored in the view rather than using `data` as a marker so that 298 | * we can properly support embedded views. Otherwise, when exiting a child view 299 | * back into the parent view, `data` will be defined and `creationMode` will be 300 | * improperly reported as false. 301 | */ 302 | CreationMode = 0b00000000100, 303 | 304 | /** 305 | * Whether or not this LView instance is on its first processing pass. 306 | * 307 | * An LView instance is considered to be on its "first pass" until it 308 | * has completed one creation mode run and one update mode run. At this 309 | * time, the flag is turned off. 310 | */ 311 | FirstLViewPass = 0b00000001000, 312 | 313 | /** Whether this view has default change detection strategy (checks always) or onPush */ 314 | CheckAlways = 0b00000010000, 315 | 316 | /** 317 | * Whether or not manual change detection is turned on for onPush components. 318 | * 319 | * This is a special mode that only marks components dirty in two cases: 320 | * 1) There has been a change to an @Input property 321 | * 2) `markDirty()` has been called manually by the user 322 | * 323 | * Note that in this mode, the firing of events does NOT mark components 324 | * dirty automatically. 325 | * 326 | * Manual mode is turned off by default for backwards compatibility, as events 327 | * automatically mark OnPush components dirty in View Engine. 328 | * 329 | * TODO: Add a public API to ChangeDetectionStrategy to turn this mode on 330 | */ 331 | ManualOnPush = 0b00000100000, 332 | 333 | /** Whether or not this view is currently dirty (needing check) */ 334 | Dirty = 0b000001000000, 335 | 336 | /** Whether or not this view is currently attached to change detection tree. */ 337 | Attached = 0b000010000000, 338 | 339 | /** Whether or not this view is destroyed. */ 340 | Destroyed = 0b000100000000, 341 | 342 | /** Whether or not this view is the root view */ 343 | IsRoot = 0b001000000000, 344 | 345 | /** 346 | * Index of the current init phase on last 22 bits 347 | */ 348 | IndexWithinInitPhaseIncrementer = 0b010000000000, 349 | IndexWithinInitPhaseShift = 10, 350 | IndexWithinInitPhaseReset = 0b001111111111, 351 | } 352 | 353 | /** 354 | * Possible states of the init phase: 355 | * - 00: OnInit hooks to be run. 356 | * - 01: AfterContentInit hooks to be run 357 | * - 10: AfterViewInit hooks to be run 358 | * - 11: All init hooks have been run 359 | */ 360 | export const enum InitPhaseState { 361 | OnInitHooksToBeRun = 0b00, 362 | AfterContentInitHooksToBeRun = 0b01, 363 | AfterViewInitHooksToBeRun = 0b10, 364 | InitPhaseCompleted = 0b11, 365 | } 366 | 367 | /** More flags associated with an LView (saved in LView[PREORDER_HOOK_FLAGS]) */ 368 | export const enum PreOrderHookFlags { 369 | /** The index of the next pre-order hook to be called in the hooks array, on the first 16 370 | bits */ 371 | IndexOfTheNextPreOrderHookMaskMask = 0b01111111111111111, 372 | 373 | /** 374 | * The number of init hooks that have already been called, on the last 16 bits 375 | */ 376 | NumberOfInitHooksCalledIncrementer = 0b010000000000000000, 377 | NumberOfInitHooksCalledShift = 16, 378 | NumberOfInitHooksCalledMask = 0b11111111111111110000000000000000, 379 | } 380 | 381 | /** 382 | * Explicitly marks `TView` as a specific type in `ngDevMode` 383 | * 384 | * It is useful to know conceptually what time of `TView` we are dealing with when 385 | * debugging an application (even if the runtime does not need it.) For this reason 386 | * we store this information in the `ngDevMode` `TView` and than use it for 387 | * better debugging experience. 388 | */ 389 | export const enum TViewType { 390 | /** 391 | * Root `TView` is the used to bootstrap components into. It is used in conjunction with 392 | * `LView` which takes an existing DOM node not owned by Angular and wraps it in `TView`/`LView` 393 | * so that other components can be loaded into it. 394 | */ 395 | Root = 0, 396 | 397 | /** 398 | * `TView` associated with a Component. This would be the `TView` directly associated with the 399 | * component view (as opposed an `Embedded` `TView` which would be a child of `Component` `TView`) 400 | */ 401 | Component = 1, 402 | 403 | /** 404 | * `TView` associated with a template. Such as `*ngIf`, `` etc... A `Component` 405 | * can have zero or more `Embedede` `TView`s. 406 | */ 407 | Embedded = 2, 408 | } 409 | 410 | /** 411 | * The static data for an LView (shared between all templates of a 412 | * given type). 413 | * 414 | * Stored on the `ComponentDef.tView`. 415 | */ 416 | export interface TView { 417 | /** 418 | * Type of `TView` (`Root`|`Component`|`Embedded`). 419 | */ 420 | type: TViewType; 421 | 422 | /** 423 | * ID for inline views to determine whether a view is the same as the previous view 424 | * in a certain position. If it's not, we know the new view needs to be inserted 425 | * and the one that exists needs to be removed (e.g. if/else statements) 426 | * 427 | * If this is -1, then this is a component view or a dynamically created view. 428 | */ 429 | readonly id: number; 430 | 431 | /** 432 | * This is a blueprint used to generate LView instances for this TView. Copying this 433 | * blueprint is faster than creating a new LView from scratch. 434 | */ 435 | blueprint: LView; 436 | 437 | /** 438 | * The template function used to refresh the view of dynamically created views 439 | * and components. Will be null for inline views. 440 | */ 441 | template: ComponentTemplate<{}>|null; 442 | 443 | /** 444 | * A function containing query-related instructions. 445 | */ 446 | viewQuery: ViewQueriesFunction<{}>|null; 447 | 448 | /** 449 | * Pointer to the host `TNode` (not part of this TView). 450 | * 451 | * If this is a `TViewNode` for an `LViewNode`, this is an embedded view of a container. 452 | * We need this pointer to be able to efficiently find this node when inserting the view 453 | * into an anchor. 454 | * 455 | * If this is a `TElementNode`, this is the view of a root component. It has exactly one 456 | * root TNode. 457 | * 458 | * If this is null, this is the view of a component that is not at root. We do not store 459 | * the host TNodes for child component views because they can potentially have several 460 | * different host TNodes, depending on where the component is being used. These host 461 | * TNodes cannot be shared (due to different indices, etc). 462 | */ 463 | node: TViewNode|TElementNode|null; 464 | 465 | /** Whether or not this template has been processed in creation mode. */ 466 | firstCreatePass: boolean; 467 | 468 | /** 469 | * Whether or not this template has been processed in update mode (e.g. change detected) 470 | * 471 | * `firstUpdatePass` is used by styling to set up `TData` to contain metadata about the styling 472 | * instructions. (Mainly to build up a linked list of styling priority order.) 473 | * 474 | * Typically this function gets cleared after first execution. If exception is thrown then this 475 | * flag can remain turned un until there is first successful (no exception) pass. This means that 476 | * individual styling instructions keep track of if they have already been added to the linked 477 | * list to prevent double adding. 478 | */ 479 | firstUpdatePass: boolean; 480 | 481 | /** Static data equivalent of LView.data[]. Contains TNodes, PipeDefInternal or TI18n. */ 482 | data: TData; 483 | 484 | /** 485 | * The binding start index is the index at which the data array 486 | * starts to store bindings only. Saving this value ensures that we 487 | * will begin reading bindings at the correct point in the array when 488 | * we are in update mode. 489 | * 490 | * -1 means that it has not been initialized. 491 | */ 492 | bindingStartIndex: number; 493 | 494 | /** 495 | * The index where the "expando" section of `LView` begins. The expando 496 | * section contains injectors, directive instances, and host binding values. 497 | * Unlike the "decls" and "vars" sections of `LView`, the length of this 498 | * section cannot be calculated at compile-time because directives are matched 499 | * at runtime to preserve locality. 500 | * 501 | * We store this start index so we know where to start checking host bindings 502 | * in `setHostBindings`. 503 | */ 504 | expandoStartIndex: number; 505 | 506 | /** 507 | * Whether or not there are any static view queries tracked on this view. 508 | * 509 | * We store this so we know whether or not we should do a view query 510 | * refresh after creation mode to collect static query results. 511 | */ 512 | staticViewQueries: boolean; 513 | 514 | /** 515 | * Whether or not there are any static content queries tracked on this view. 516 | * 517 | * We store this so we know whether or not we should do a content query 518 | * refresh after creation mode to collect static query results. 519 | */ 520 | staticContentQueries: boolean; 521 | 522 | /** 523 | * A reference to the first child node located in the view. 524 | */ 525 | firstChild: TNode|null; 526 | 527 | /** 528 | * Set of instructions used to process host bindings efficiently. 529 | * 530 | * See VIEW_DATA.md for more information. 531 | */ 532 | // TODO(misko): `expandoInstructions` should be renamed to `hostBindingsInstructions` since they 533 | // keep track of `hostBindings` which need to be executed. 534 | expandoInstructions: any|null; 535 | 536 | /** 537 | * Full registry of directives and components that may be found in this view. 538 | * 539 | * It's necessary to keep a copy of the full def list on the TView so it's possible 540 | * to render template functions without a host component. 541 | */ 542 | directiveRegistry: DirectiveDefList|null; 543 | 544 | /** 545 | * Full registry of pipes that may be found in this view. 546 | * 547 | * The property is either an array of `PipeDefs`s or a function which returns the array of 548 | * `PipeDefs`s. The function is necessary to be able to support forward declarations. 549 | * 550 | * It's necessary to keep a copy of the full def list on the TView so it's possible 551 | * to render template functions without a host component. 552 | */ 553 | pipeRegistry: PipeDefList|null; 554 | 555 | /** 556 | * Array of ngOnInit, ngOnChanges and ngDoCheck hooks that should be executed for this view in 557 | * creation mode. 558 | * 559 | * Even indices: Directive index 560 | * Odd indices: Hook function 561 | */ 562 | preOrderHooks: HookData|null; 563 | 564 | /** 565 | * Array of ngOnChanges and ngDoCheck hooks that should be executed for this view in update mode. 566 | * 567 | * Even indices: Directive index 568 | * Odd indices: Hook function 569 | */ 570 | preOrderCheckHooks: HookData|null; 571 | 572 | /** 573 | * Array of ngAfterContentInit and ngAfterContentChecked hooks that should be executed 574 | * for this view in creation mode. 575 | * 576 | * Even indices: Directive index 577 | * Odd indices: Hook function 578 | */ 579 | contentHooks: HookData|null; 580 | 581 | /** 582 | * Array of ngAfterContentChecked hooks that should be executed for this view in update 583 | * mode. 584 | * 585 | * Even indices: Directive index 586 | * Odd indices: Hook function 587 | */ 588 | contentCheckHooks: HookData|null; 589 | 590 | /** 591 | * Array of ngAfterViewInit and ngAfterViewChecked hooks that should be executed for 592 | * this view in creation mode. 593 | * 594 | * Even indices: Directive index 595 | * Odd indices: Hook function 596 | */ 597 | viewHooks: HookData|null; 598 | 599 | /** 600 | * Array of ngAfterViewChecked hooks that should be executed for this view in 601 | * update mode. 602 | * 603 | * Even indices: Directive index 604 | * Odd indices: Hook function 605 | */ 606 | viewCheckHooks: HookData|null; 607 | 608 | /** 609 | * Array of ngOnDestroy hooks that should be executed when this view is destroyed. 610 | * 611 | * Even indices: Directive index 612 | * Odd indices: Hook function 613 | */ 614 | destroyHooks: HookData|null; 615 | 616 | /** 617 | * When a view is destroyed, listeners need to be released and outputs need to be 618 | * unsubscribed. This cleanup array stores both listener data (in chunks of 4) 619 | * and output data (in chunks of 2) for a particular view. Combining the arrays 620 | * saves on memory (70 bytes per array) and on a few bytes of code size (for two 621 | * separate for loops). 622 | * 623 | * If it's a native DOM listener or output subscription being stored: 624 | * 1st index is: event name `name = tView.cleanup[i+0]` 625 | * 2nd index is: index of native element or a function that retrieves global target (window, 626 | * document or body) reference based on the native element: 627 | * `typeof idxOrTargetGetter === 'function'`: global target getter function 628 | * `typeof idxOrTargetGetter === 'number'`: index of native element 629 | * 630 | * 3rd index is: index of listener function `listener = lView[CLEANUP][tView.cleanup[i+2]]` 631 | * 4th index is: `useCaptureOrIndx = tView.cleanup[i+3]` 632 | * `typeof useCaptureOrIndx == 'boolean' : useCapture boolean 633 | * `typeof useCaptureOrIndx == 'number': 634 | * `useCaptureOrIndx >= 0` `removeListener = LView[CLEANUP][useCaptureOrIndx]` 635 | * `useCaptureOrIndx < 0` `subscription = LView[CLEANUP][-useCaptureOrIndx]` 636 | * 637 | * If it's an output subscription or query list destroy hook: 638 | * 1st index is: output unsubscribe function / query list destroy function 639 | * 2nd index is: index of function context in LView.cleanupInstances[] 640 | * `tView.cleanup[i+0].call(lView[CLEANUP][tView.cleanup[i+1]])` 641 | */ 642 | cleanup: any[]|null; 643 | 644 | /** 645 | * A list of element indices for child components that will need to be 646 | * refreshed when the current view has finished its check. These indices have 647 | * already been adjusted for the HEADER_OFFSET. 648 | * 649 | */ 650 | components: number[]|null; 651 | 652 | /** 653 | * A collection of queries tracked in a given view. 654 | */ 655 | queries: any|null; 656 | 657 | /** 658 | * An array of indices pointing to directives with content queries alongside with the 659 | * corresponding 660 | * query index. Each entry in this array is a tuple of: 661 | * - index of the first content query index declared by a given directive; 662 | * - index of a directive. 663 | * 664 | * We are storing those indexes so we can refresh content queries as part of a view refresh 665 | * process. 666 | */ 667 | contentQueries: number[]|null; 668 | 669 | /** 670 | * Set of schemas that declare elements to be allowed inside the view. 671 | */ 672 | schemas: SchemaMetadata[]|null; 673 | 674 | /** 675 | * Array of constants for the view. Includes attribute arrays, local definition arrays etc. 676 | * Used for directive matching, attribute bindings, local definitions and more. 677 | */ 678 | consts: TConstants|null; 679 | } 680 | 681 | export const enum RootContextFlags {Empty = 0b00, DetectChanges = 0b01, FlushPlayers = 0b10} 682 | 683 | 684 | /** 685 | * RootContext contains information which is shared for all components which 686 | * were bootstrapped with {@link renderComponent}. 687 | */ 688 | export interface RootContext { 689 | /** 690 | * A function used for scheduling change detection in the future. Usually 691 | * this is `requestAnimationFrame`. 692 | */ 693 | scheduler: (workFn: () => void) => void; 694 | 695 | /** 696 | * A promise which is resolved when all components are considered clean (not dirty). 697 | * 698 | * This promise is overwritten every time a first call to {@link markDirty} is invoked. 699 | */ 700 | clean: Promise; 701 | 702 | /** 703 | * RootComponents - The components that were instantiated by the call to 704 | * {@link renderComponent}. 705 | */ 706 | components: {}[]; 707 | 708 | /** 709 | * The player flushing handler to kick off all animations 710 | */ 711 | playerHandler: PlayerHandler|null; 712 | 713 | /** 714 | * What render-related operations to run once a scheduler has been set 715 | */ 716 | flags: RootContextFlags; 717 | } 718 | 719 | /** 720 | * Array of hooks that should be executed for a view and their directive indices. 721 | * 722 | * For each node of the view, the following data is stored: 723 | * 1) Node index (optional) 724 | * 2) A series of number/function pairs where: 725 | * - even indices are directive indices 726 | * - odd indices are hook functions 727 | * 728 | * Special cases: 729 | * - a negative directive index flags an init hook (ngOnInit, ngAfterContentInit, ngAfterViewInit) 730 | */ 731 | export type HookData = (number | (() => void))[]; 732 | 733 | /** 734 | * Static data that corresponds to the instance-specific data array on an LView. 735 | * 736 | * Each node's static data is stored in tData at the same index that it's stored 737 | * in the data array. Any nodes that do not have static data store a null value in 738 | * tData to avoid a sparse array. 739 | * 740 | * Each pipe's definition is stored here at the same index as its pipe instance in 741 | * the data array. 742 | * 743 | * Each host property's name is stored here at the same index as its value in the 744 | * data array. 745 | * 746 | * Each property binding name is stored here at the same index as its value in 747 | * the data array. If the binding is an interpolation, the static string values 748 | * are stored parallel to the dynamic values. Example: 749 | * 750 | * id="prefix {{ v0 }} a {{ v1 }} b {{ v2 }} suffix" 751 | * 752 | * LView | TView.data 753 | *------------------------ 754 | * v0 value | 'a' 755 | * v1 value | 'b' 756 | * v2 value | id � prefix � suffix 757 | * 758 | * Injector bloom filters are also stored here. 759 | */ 760 | export type TData = 761 | (TNode | PipeDef| DirectiveDef| ComponentDef| number | Type| InjectionToken| null | string)[]; 762 | 763 | // Note: This hack is necessary so we don't erroneously get a circular dependency 764 | // failure based on types. 765 | export const unusedValueExportToPlacateAjd = 1; 766 | --------------------------------------------------------------------------------