import { Component, ChangeDetectionStrategy } from '@angular/core';
import { IGrid, ICell } from '../interfaces/models.ts';
import { CellDataService } from '../services/celldataservice.ts';
@Component({
selector: 'my-app',
template: `<my-grid [data]=grid class="grid"></my-grid>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
name = 'Angular 123';
private grid: IGrid;
constructor(cellDataService: CellDataService) {
this.grid = cellDataService.getGridState();
// console.log("app - grid", this.grid.cells);
}
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import {CellDataService} from '../services/celldataservice.ts';
import { GridComponent } from '../components/grid.ts';
import { CellComponent } from '../components/cell.ts';
@NgModule({
imports: [ BrowserModule ],
declarations: [
AppComponent,
GridComponent,
CellComponent
],
providers: [CellDataService],
bootstrap: [ AppComponent ]
})
export class AppModule { }
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<html>
<head>
<title>Angular Quickstart</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
box-sizing: border-box;
}
.hide {
display: none;
}
body {
background:#1c1d1f;
color: #FFF;
font-size: 1em;
font-family: Helvetica,Georgia,sans-serif;
margin-left:20px;
}
h1 {
font-size: 1.3em;
color:#FFC600;
}
p {
font-size: 0.9em;
color: #FFF;
font-family: consolas,sans-serif;
}
.app {
perspective: 500px;
display: block;
}
</style>
<!-- Polyfills -->
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
<script src="https://unpkg.com/zone.js@0.8.4?main=browser"></script>
<script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<h1>OnPush Change Detection</h1>
<p> Click on the cell to visualize change detection with onpush ChangeDetectionStrategy.
Border appears around the cell when Angular runs change detection for that cell.</p>
<my-app class="app">loading...</my-app>
</body>
</html>
(function (global) {
System.config({
transpiler: 'ts',
typescriptOptions: {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2015", "dom"],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
meta: {
'typescript': {
"exports": "ts"
}
},
paths: {
'npm:': 'https://unpkg.com/'
},
map: {
'app': 'app',
'@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js',
'@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
'rxjs': 'npm:rxjs@5.0.1',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.2.1/lib/typescript.js',
'lodash': 'npm:lodash@4.17.4/lodash.js',
},
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts',
meta: {
'./*.ts': {
loader: 'systemjs-angular-loader.js'
}
}
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm;
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
module.exports.translate = function(load){
if (load.source.indexOf('moduleId') != -1) return load;
var url = document.createElement('a');
url.href = load.address;
var basePathParts = url.pathname.split('/');
basePathParts.pop();
var basePath = basePathParts.join('/');
var baseHref = document.createElement('a');
baseHref.href = this.baseURL;
baseHref = baseHref.pathname;
if (!baseHref.startsWith('/base/')) { // it is not karma
basePath = basePath.replace(baseHref, '');
}
load.source = load.source
.replace(templateUrlRegex, function(match, quote, url){
let resolvedUrl = url;
if (url.startsWith('.')) {
resolvedUrl = basePath + url.substr(1);
}
return 'templateUrl: "' + resolvedUrl + '"';
})
.replace(stylesRegex, function(match, relativeUrls) {
var urls = [];
while ((match = stringRegex.exec(relativeUrls)) !== null) {
if (match[2].startsWith('.')) {
urls.push('"' + basePath + match[2].substr(1) + '"');
} else {
urls.push('"' + match[2] + '"');
}
}
return "styleUrls: [" + urls.join(', ') + "]";
});
return load;
};
import { Component, Input, OnInit, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core';
import { IGrid } from '../interfaces/models.ts';
@Component({
selector: 'my-grid',
templateUrl: './components/grid.html',
styleUrls: [ './components/grid.css' ],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GridComponent implements OnInit {
@Input() data: IGrid;
constructor() { }
ngOnInit(): void {
}
}
export interface IGrid {
cells: Cell[];
rows: number;
columns: number;
}
// Angular CLI had a bug which prevents to export multiple interfaces
// using class
export class ICell {
id: number;
flipped: boolean;
text: string;
changed: boolean;
}
/**
* Bitmask of states
* https://github.com/angular/angular/blob/39b92f7e546b8bdecc0e08c915d36717358c122b/packages/core/src/view/types.ts
*/
export const enum ViewState {
BeforeFirstCheck = 1 << 0,
FirstCheck = 1 << 1,
Attached = 1 << 2,
ChecksEnabled = 1 << 3,
IsProjectedView = 1 << 4,
CheckProjectedView = 1 << 5,
CheckProjectedViews = 1 << 6,
Destroyed = 1 << 7,
CatDetectChanges = Attached | ChecksEnabled,
CatInit = BeforeFirstCheck | CatDetectChanges
}
import { Component,
Input,
OnInit,
AfterViewChecked,
ViewEncapsulation,
NgZone,
ElementRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
DoCheck } from '@angular/core';
import { ICell, ViewState } from '../interfaces/models.ts';
import {CellDataService} from '../services/celldataservice.ts';
@Component({
selector: 'my-cell',
templateUrl: './components/cell.html',
styleUrls: [ './components/cell.css' ],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CellComponent implements OnInit, AfterViewChecked, DoCheck {
@Input() data: ICell;
constructor(
private cellDataService: CellDataService,
private zone: NgZone,
private el:ElementRef,
private ref: ChangeDetectorRef) { }
ngOnInit(): void {
}
ngAfterViewChecked() {
// "ngAfterViewChecked" is always getting executed (irrespective of Default or OnPush) making it not useful to trigger the visualization.
// Angular had a bug on this - https://github.com/angular/angular/issues/7055
// Using a work-around the visualize the change detection.
}
ngDoCheck() {
}
onClick(): void {
this.cellDataService.flipCell(this.data.id);
this.zone.runOutsideAngular(() => {
setTimeout(() => {
// Since Angular doesn't detect any of these changes (due to onpush),
// Reset the 'changed' state outside Angular
const elem = document.querySelectorAll(`[data-id="${this.data.id}"]`)[0];
if (!elem) { return;}
elem.classList.remove('changed');
this.cellDataService.resetChanged();
}, 1200);
});
}
}
<my-cell *ngFor="let cell of data.cells"
[data]=cell class="cell"
[class.flip]="cell.flip"></my-cell>
<div [attr.data-id]="data.id"
(click)="onClick()"
[class.changed]="data.changed">
<div class="front"></div>
<div class="back">{{data.text}}</div>
</div>
.grid {
width: 900px;
display: block;
position:absolute;
left:170px;
top: 30px;
transform: rotateX(0deg) rotateY(10deg) rotateZ(0deg);
transform-style: preserve-3d;
}
.cell {
width:30px;
height:30px;
float:left;
margin:0 5px 5px 0;
cursor:pointer;
display: block;
transition: all 0.4s;
transform-style: preserve-3d;
position: relative;
}
.cell.flip {
transform: rotateY(180deg);
}
.front {
background:#75A2A2;
}
.back {
background:#FFC600;
transform: rotateY(180deg);
box-shadow:10px 10px 10px rgba(0,0,0,0.5);
color: black;
text-align: center;
padding-top:25%;
}
.front, .back {
width:100%;
height:100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
position: absolute;
top: 0;
left: 0;
}
.changed .front {
border: 3px dashed #FFC600;
}
.changed .back {
border: 3px dashed #75A2A2;
}
/*
.cell {
width:30px;
height:30px;
float:left;
margin:0 5px 5px 0;
cursor:pointer;
display: block;
}
.cell:before{
position:absolute;
content:'';
width:30px;
height:30px;
background:#75A2A2;
transition: all 0.4s;
pointer-events: none;
transform-style: preserve-3d;
}
.cell:hover:before{
background:#FFC600;
transform: rotateY(180deg);
box-shadow:10px 10px 10px rgba(0,0,0,0.5);
}
*/
//import { Hero } from './hero';
import { Injectable } from '@angular/core';
import * as _ from 'lodash';
import { IGrid, ICell } from '../interfaces/models.ts';
@Injectable()
export class CellDataService {
readonly TOTAL: number = 500;
private currentState: IGrid;
constructor() {
this.currentState = {
cells:_.range(this.TOTAL).map(n => {
return <ICell> {
id: n,
flip: false,
text: "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"[Math.floor(Math.random() * 36)],
changed: false
};
})
};
console.log(this.currentState);
}
getGridState(): IGrid {
return this.currentState;
}
flipCell(id): void {
console.log(id);
let index: number = _.findIndex(this.currentState.cells, c => c.id === id);
let updatedCell = Object.assign({}, this.currentState.cells[index], {
flip: !this.currentState.cells[index].flip
});
this.currentState.cells = [
...this.currentState.cells.slice(0, index),
updatedCell,
...this.currentState.cells.slice(index + 1)
];
// To trigger the 'changed' visualization
_.each(this.currentState.cells, c => { c.changed = true; })
}
resetChanged(): void {
// To reset the 'changed' visualization
_.each(this.currentState.cells, c => { c.changed = false; })
}
addCells(count: number): IGrid {
}
removeCells(count: number): IGrid {
}
}
/*
export interface IGrid {
cells: Cell[];
rows: number;
columns: number;
}
// Angular CLI had a bug which prevents to export multiple interfaces
// using class
export class ICell {
id: number;
width: number;
height: number;
flipped: boolean;
}
*/