Skip to content

Commit

Permalink
fix(plugin): index.component to support AOT
Browse files Browse the repository at this point in the history
  • Loading branch information
JonnyBGod committed Jan 6, 2017
1 parent 24238e8 commit fae88a5
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 144 deletions.
2 changes: 1 addition & 1 deletion dist/plugin/index.component.metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"__symbolic":"module","version":1,"metadata":{"ScrollSpyIndexRenderComponent":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Injectable"}},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Component"},"arguments":[{"selector":"scrollSpy-index-render","template":"<div #container></div>","changeDetection":{"__symbolic":"select","expression":{"__symbolic":"reference","module":"@angular/core","name":"ChangeDetectionStrategy"},"member":"OnPush"}}]}],"members":{"scrollSpyIndexRenderOptions":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"viewContainerRef":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"ViewChild"},"arguments":["container",{"read":{"__symbolic":"reference","module":"@angular/core","name":"ViewContainerRef"}}]}]}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"Compiler"},{"__symbolic":"reference","module":"@angular/core","name":"ChangeDetectorRef"},{"__symbolic":"reference","module":"@angular/core","name":"ComponentFactoryResolver"},{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"},{"__symbolic":"reference","module":"../index","name":"ScrollSpyService"},{"__symbolic":"reference","module":"./index.service","name":"ScrollSpyIndexService"}]}],"ngOnInit":[{"__symbolic":"method"}],"ngAfterViewInit":[{"__symbolic":"method"}],"update":[{"__symbolic":"method"}],"itemConstruct":[{"__symbolic":"method"}],"calculateHighlight":[{"__symbolic":"method"}],"getItemsToHighlight":[{"__symbolic":"method"}],"compileToComponent":[{"__symbolic":"method"}],"ngOnDestroy":[{"__symbolic":"method"}]}}}}
{"__symbolic":"module","version":1,"metadata":{"ScrollSpyIndexRenderComponent":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Injectable"}},{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Component"},"arguments":[{"selector":"scrollSpy-index-render","template":"\n <div #container>\n <ul class=\"nav menu\">\n <li *ngFor=\"let item of items\" [class.active]=\"highlight(item.link)\">\n <a [routerLink]=\"\" fragment=\"{{item.link}}\" (click)=\"goTo(item.link)\">{{item.text}}</a>\n <ul *ngIf=\"item.children.length\" class=\"nav menu\">\n <li *ngFor=\"let itemChild of item.children\" [class.active]=\"highlight(itemChild.link)\">\n <a [routerLink]=\"\" fragment=\"{{itemChild.link}}\" (click)=\"goTo(itemChild.link)\">{{itemChild.text}}</a>\n <ul *ngIf=\"itemChild.children.length\" class=\"nav menu\">\n <li *ngFor=\"let itemChild1 of itemChild.children\" [class.active]=\"highlight(itemChild1.link)\">\n <a [routerLink]=\"\" fragment=\"{{itemChild1.link}}\" (click)=\"goTo(itemChild1.link)\">{{itemChild1.text}}</a>\n <ul *ngIf=\"itemChild1.children.length\" class=\"nav menu\">\n <li *ngFor=\"let itemChild2 of itemChild1.children\" [class.active]=\"highlight(itemChild2.link)\">\n <a [routerLink]=\"\" fragment=\"{{itemChild2.link}}\" (click)=\"goTo(itemChild2.link)\">{{itemChild2.text}}</a>\n </li>\n </ul>\n </li>\n </ul>\n </li>\n </ul>\n </li>\n </ul>\n </div>\n ","changeDetection":{"__symbolic":"select","expression":{"__symbolic":"reference","module":"@angular/core","name":"ChangeDetectionStrategy"},"member":"OnPush"}}]}],"members":{"scrollSpyIndexRenderOptions":[{"__symbolic":"property","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"Input"}}]}],"__ctor__":[{"__symbolic":"constructor","parameters":[{"__symbolic":"reference","module":"@angular/core","name":"ChangeDetectorRef"},{"__symbolic":"reference","module":"@angular/core","name":"ElementRef"},{"__symbolic":"reference","module":"../index","name":"ScrollSpyService"},{"__symbolic":"reference","module":"./index.service","name":"ScrollSpyIndexService"}]}],"ngOnInit":[{"__symbolic":"method"}],"ngAfterViewInit":[{"__symbolic":"method"}],"update":[{"__symbolic":"method"}],"calculateHighlight":[{"__symbolic":"method"}],"highlight":[{"__symbolic":"method"}],"goTo":[{"__symbolic":"method"}],"ngOnDestroy":[{"__symbolic":"method"}]}}}}
28 changes: 20 additions & 8 deletions src/plugin/index.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ describe('plugin index.render.directive', () => {
advance(fixture);

let compiled = fixture.debugElement.nativeElement;
let match = compiled.getElementsByTagName('scrollspymenu')[0].outerHTML;
let match = compiled.getElementsByTagName('scrollspy-index-render')[0].outerHTML;

expect(match).toEqual('<scrollspymenu><ul class="nav menu"><li pagemenuspy="test1" parent="" class="active"><a fragment="test1" ng-reflect-fragment="test1" ng-reflect-href="/#test1" href="/#test1">test1</a></li><li pagemenuspy="test2" parent=""><a fragment="test2" ng-reflect-fragment="test2" ng-reflect-href="/#test2" href="/#test2">test2</a></li><li pagemenuspy="test3" parent=""><a fragment="test3" ng-reflect-fragment="test3" ng-reflect-href="/#test3" href="/#test3">test3</a></li></ul></scrollspymenu>');
expect(match).toContain('<a ng-reflect-fragment="test1" ng-reflect-href="/#test1" href="/#test1">test1</a>');
expect(match).toContain('<a ng-reflect-fragment="test2" ng-reflect-href="/#test2" href="/#test2">test2</a>');
expect(match).toContain('<a ng-reflect-fragment="test3" ng-reflect-href="/#test3" href="/#test3">test3</a>');
expect(match).toContain('<a ng-reflect-fragment="test3-child" ng-reflect-href="/#test3-child" href="/#test3-child">test3-child</a>');
expect(match).toContain('<a ng-reflect-fragment="test3-child-child" ng-reflect-href="/#test3-child-child" href="/#test3-child-child">test3-child-child</a>');
})));

it('should highlight base on spyId',
Expand All @@ -57,15 +61,18 @@ describe('plugin index.render.directive', () => {
advance(fixture);

let match = compiled.getElementsByClassName('active')[0].outerHTML;
expect(match).toEqual('<li pagemenuspy="test1" parent="" class="active"><a fragment="test1" ng-reflect-fragment="test1" ng-reflect-href="/#test1" href="/#test1">test1</a></li>');
expect(match).toContain('<a ng-reflect-fragment="test1" ng-reflect-href="/#test1" href="/#test1">test1</a>');

window.scrollTo(0, 1100);
window.scrollTo(0, 3100);
evt.initUIEvent('scroll', true, true, window, 1);
window.dispatchEvent(evt);
advance(fixture);

match = compiled.getElementsByClassName('active')[0].outerHTML;
expect(match).toEqual('<li pagemenuspy="test2" parent="" class="active"><a fragment="test2" ng-reflect-fragment="test2" ng-reflect-href="/#test2" href="/#test2">test2</a></li>');
match = compiled.getElementsByClassName('active');

expect(match.length).toEqual(2);
expect(match[0].outerHTML).toContain('<a ng-reflect-fragment="test3" ng-reflect-href="/#test3" href="/#test3">test3</a>');
expect(match[1].outerHTML).toContain('<a ng-reflect-fragment="test3-child" ng-reflect-href="/#test3-child" href="/#test3-child">test3-child</a>');
})));

it('should highlight respecting topMargin',
Expand All @@ -82,15 +89,15 @@ describe('plugin index.render.directive', () => {
advance(fixture);

let match = compiled.getElementsByClassName('active')[0].outerHTML;
expect(match).toEqual('<li pagemenuspy="test1" parent="" class="active"><a fragment="test1" ng-reflect-fragment="test1" ng-reflect-href="/#test1" href="/#test1">test1</a></li>');
expect(match).toContain('<a ng-reflect-fragment="test1" ng-reflect-href="/#test1" href="/#test1">test1</a>');

window.scrollTo(0, 900);
evt.initUIEvent('scroll', true, true, window, 1);
window.dispatchEvent(evt);
advance(fixture);

match = compiled.getElementsByClassName('active')[0].outerHTML;
expect(match).toEqual('<li pagemenuspy="test2" parent="" class="active"><a fragment="test2" ng-reflect-fragment="test2" ng-reflect-href="/#test2" href="/#test2">test2</a></li>');
expect(match).toContain('<a ng-reflect-fragment="test2" ng-reflect-href="/#test2" href="/#test2">test2</a>');
})));
});

Expand All @@ -102,6 +109,11 @@ describe('plugin index.render.directive', () => {
<h3 id="test1" class="anchor" style="height: 1000px;">test1</h3>
<h3 id="test2" class="anchor" style="height: 1000px;">test2</h3>
<h3 id="test3" class="anchor" style="height: 1000px;">test3</h3>
<h4 id="test3-child" class="anchor" style="height: 1000px;">test3-child</h4>
<h5 id="test3-child-child" class="anchor" style="height: 1000px;">test3-child-child</h5>
<h3 id="test4" class="anchor" style="height: 1000px;">test4</h3>
<h4 id="test4-child" class="anchor" style="height: 1000px;">test4-child</h4>
<h5 id="test4-child-child" class="anchor" style="height: 1000px;">test4-child-child</h5>
</div>
<scrollSpy-index-render [scrollSpyIndexRenderOptions]="{id: 'test', spyId: 'window', topMargin: -200}"></scrollSpy-index-render>
</div>
Expand Down
234 changes: 99 additions & 135 deletions src/plugin/index.component.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import {
NgModule,
Component,
Compiler,
ChangeDetectorRef,
Injectable,
Input,
ElementRef,
ComponentFactoryResolver,
OnInit,
AfterViewInit,
ViewContainerRef,
ViewChild,
OnDestroy,
ChangeDetectionStrategy
} from '@angular/core';
import { RouterModule } from '@angular/router';

import { ScrollSpyService } from '../index';
import { ScrollSpyIndexService } from './index.service';
Expand All @@ -28,21 +22,39 @@ export interface ScrollSpyIndexComponentOptions {
@Injectable()
@Component({
selector: 'scrollSpy-index-render',
template: `<div #container></div>`,
template: `
<div #container>
<ul class="nav menu">
<li *ngFor="let item of items" [class.active]="highlight(item.link)">
<a [routerLink]="" fragment="{{item.link}}" (click)="goTo(item.link)">{{item.text}}</a>
<ul *ngIf="item.children.length" class="nav menu">
<li *ngFor="let itemChild of item.children" [class.active]="highlight(itemChild.link)">
<a [routerLink]="" fragment="{{itemChild.link}}" (click)="goTo(itemChild.link)">{{itemChild.text}}</a>
<ul *ngIf="itemChild.children.length" class="nav menu">
<li *ngFor="let itemChild1 of itemChild.children" [class.active]="highlight(itemChild1.link)">
<a [routerLink]="" fragment="{{itemChild1.link}}" (click)="goTo(itemChild1.link)">{{itemChild1.text}}</a>
<ul *ngIf="itemChild1.children.length" class="nav menu">
<li *ngFor="let itemChild2 of itemChild1.children" [class.active]="highlight(itemChild2.link)">
<a [routerLink]="" fragment="{{itemChild2.link}}" (click)="goTo(itemChild2.link)">{{itemChild2.text}}</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ScrollSpyIndexRenderComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() public scrollSpyIndexRenderOptions: ScrollSpyIndexComponentOptions;

public stack: Array<any> = [];
public parentStack: Array<any> = [];
public lastItem: any;

public currentScrollPosition: number;
public items: any[] = [];
public itemsHash: any = {};
public itemsToHighlight: Array<string> = [];

@ViewChild('container', { read: ViewContainerRef })
public viewContainerRef: ViewContainerRef;

public defaultOptions: ScrollSpyIndexComponentOptions = {
spyId: 'window',
Expand All @@ -55,9 +67,7 @@ export class ScrollSpyIndexRenderComponent implements OnInit, AfterViewInit, OnD
public el: HTMLElement;

constructor(
private compiler: Compiler,
private ref: ChangeDetectorRef,
private resolver: ComponentFactoryResolver,
private elRef: ElementRef,
private scrollSpy: ScrollSpyService,
private scrollSpyIndex: ScrollSpyIndexService
Expand Down Expand Up @@ -103,90 +113,81 @@ export class ScrollSpyIndexRenderComponent implements OnInit, AfterViewInit, OnD
}

update() {
var items: Array<any> = this.scrollSpyIndex.getIndex(this.scrollSpyIndexRenderOptions.id) || [];
var markup: string = '<ul class="nav menu">';

for (var i = 0; i < items.length; i++) {
var item = this.itemConstruct(items[i]);

if (item.push) {
markup += '<ul class="nav menu">';
} else if (item.pop) {
for (var j = 0; j < item.pop; j++) {
markup += '</li></ul>';
const data: Array<any> = this.scrollSpyIndex.getIndex(this.scrollSpyIndexRenderOptions.id) || [];

let stack: Array<any> = [];
let parentStack: Array<any> = [];
let lastItem: any;

this.items = [];
this.itemsHash = {};

for (var i = 0; i < data.length; ++i) {
// parse basic info from the dom item
var item: any = {
link: data[i].id,
text: data[i].textContent || data[i].innerText,
parents: [],
children: []
};

// build type identifier
var level: string = data[i].tagName;
for (var n = 0; n < data[i].classList.length; n++) {
level += ',' + data[i].classList[n];
}

// here be dragons
var stacksize: number = stack.length;
if (stacksize === 0) {
// we are at the top level and will stay there
stack.push(level);
} else if (level !== stack[stacksize - 1]) {
// traverse the ancestry, looking for a match
for (var j = stacksize - 1; j >= 0; j--) {
if (level === stack[j]) {
break; // found an ancestor
}
}
if (j < 0) {
// this is a new submenu item, lets push the stack
stack.push(level);
parentStack.push(lastItem);
} else {
// we are either a sibling or higher up the tree,
// lets pop the stack if needed
while (stack.length > j + 1) {
stack.pop();
parentStack.pop();
}
}
} else if (i !== 0) {
markup += '</li>';
}

markup += '<li [class.active]="highlight(\'' + item.link + '\')" pagemenuspy="' + item.link + '" parent="' + item.parent + '">';

// HACK: remove click once https://github.com/angular/angular/issues/6595 is fixed
markup += '<a [routerLink]="" fragment="' + item.link + '" (click)="goTo(\'' + item.link + '\')">';
markup += item.text;
markup += '</a>';
}
markup += '</ul>';

this.viewContainerRef.clear();
let componentFactory = this.compileToComponent(markup, () => this.getItemsToHighlight());
this.viewContainerRef.createComponent(componentFactory);

setTimeout(() => {
this.calculateHighlight();
});
}

itemConstruct(data: any) {
// parse basic info from the dom item
var item: any = {
link: data.id,
text: data.textContent || data.innerText,
parent: ''
};

// build type identifier
var level: string = data.tagName;
for (var i = 0; i < data.classList.length; i++) {
level += ',' + data.classList[i];
}

// here be dragons
var stacksize: number = this.stack.length;
if (stacksize === 0) {
// we are at the top level and will stay there
this.stack.push(level);
} else if (level !== this.stack[stacksize - 1]) {
// traverse the ancestry, looking for a match
for (var j = stacksize - 1; j >= 0; j--) {
if (level === this.stack[j]) {
break; // found an ancestor
// for next iteration
lastItem = item.link;

// if we have a parent, lets record it
if (parentStack.length > 0) {
item.parents = [...parentStack];

let temp: any = this.items;
for (var t = 0; t < parentStack.length; ++t) {
if (t < parentStack.length - 1) {
temp = temp.filter((e: any) => { return e.link === parentStack[t]; })[0].children;
} else {
temp.filter((e: any) => { return e.link === parentStack[t]; })[0].children.push(item);
}
}
}
if (j < 0) {
// this is a new submenu item, lets push the this.stack
this.stack.push(level);
item.push = true;
this.parentStack.push(this.lastItem);
} else {
// we are either a sibling or higher up the tree,
// lets pop the this.stack if needed
item.pop = stacksize - 1 - j;
while (this.stack.length > j + 1) {
this.stack.pop();
this.parentStack.pop();
}
this.items.push(item);
}
}

// if we have a parent, lets record it
if (this.parentStack.length > 0) {
item.parent = this.parentStack[this.parentStack.length - 1];
this.itemsHash[item.link] = item;
}

// for next iteration
this.lastItem = item.link;
return item;
setTimeout(() => {
this.calculateHighlight();
});
}

calculateHighlight() {
Expand All @@ -208,56 +209,19 @@ export class ScrollSpyIndexRenderComponent implements OnInit, AfterViewInit, OnD
if (!highlightItem) {
highlightItem = items[0].id;
}
this.itemsToHighlight.push(highlightItem);

while (!!highlightItem) {
var item = this.el.querySelector('[pagemenuspy=' + highlightItem + ']');
if (!!item) {
var parent = item.getAttribute('parent');
if (parent) {
highlightItem = parent;
this.itemsToHighlight.push(highlightItem);
} else {
highlightItem = null;
}
} else {
highlightItem = null;
}
}
this.itemsToHighlight = [highlightItem, ...this.itemsHash[highlightItem].parents];

this.ref.markForCheck();
}

getItemsToHighlight(): Array<string> {
return this.itemsToHighlight;
highlight(id: string): boolean {
return this.itemsToHighlight.indexOf(id) !== -1;
}

compileToComponent(template: string, itemsToHighlight: any): any {
@Injectable()
@Component({
selector: 'scrollSpyMenu',
template
})
class RenderComponent {
highlight(id: string): boolean {
return itemsToHighlight().indexOf(id) !== -1;
}

// HACK: remove click once https://github.com/angular/angular/issues/6595 is fixed
goTo(anchor: string) {
setTimeout(() => {
document.querySelector('#' + anchor).scrollIntoView();
});
}
};

@NgModule({imports: [RouterModule], declarations: [RenderComponent]})
class RenderModule {}

return this.compiler.compileModuleAndAllComponentsSync(RenderModule)
.componentFactories.find((comp) =>
comp.componentType === RenderComponent
);
goTo(anchor: string) {
setTimeout(() => {
document.querySelector('#' + anchor).scrollIntoView();
});
}

ngOnDestroy() {
Expand Down

0 comments on commit fae88a5

Please sign in to comment.