Skip to content

Commit ce599ed

Browse files
authored
Sander/css scoping (#250)
* feat(cssscoping): you can now use angular styling for blog posts This PR enables the possibility to use the angular CSS styles from the holding component in the blog posts * feat(blogcomponentcss): update to show the scoped CSS by making a special H1
1 parent 4ae475e commit ce599ed

File tree

10 files changed

+130
-98
lines changed

10 files changed

+130
-98
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"cSpell.words": ["Scully", "Scullyio"],
2+
"cSpell.words": ["Scully", "Scullyio", "ngcontent"],
33
"peacock.affectActivityBar": true,
44
"peacock.affectTitleBar": true,
55
"peacock.affectStatusBar": true,

projects/sampleBlog/src/app/blog/blog-list/blog-list.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {map} from 'rxjs/operators';
99
})
1010
export class BlogListComponent implements OnInit {
1111
blogs$ = this.srs.available$.pipe(
12-
map(routeList => routeList.filter((route: ScullyRoute) => route.route.startsWith(`/blog/`)))
12+
map(routeList => routeList.filter((route: ScullyRoute) => route.route.startsWith(`/blog/`))),
13+
map(blogs => blogs.sort((a, b) => (a.date < b.date ? -1 : 1)))
1314
);
1415

1516
constructor(private srs: ScullyRoutesService) {}

projects/sampleBlog/src/app/blog/blog.component.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
::slotted(h1) {
1+
h1 {
22
color: rgb(51, 6, 37);
33
background-color: rgb(248, 211, 236);
44
padding: 5px;

projects/scullyio/ng-lib/src/lib/scully-content/scully-content.component.ts

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,26 @@ import {
22
ChangeDetectionStrategy,
33
Component,
44
ElementRef,
5-
Input,
65
OnDestroy,
76
OnInit,
87
ViewEncapsulation,
98
} from '@angular/core';
109
import {Router} from '@angular/router';
11-
import {Observable, Subscription} from 'rxjs';
1210
import {take} from 'rxjs/operators';
1311
import {IdleMonitorService} from '../idleMonitor/idle-monitor.service';
1412
import {ScullyRoutesService} from '../route-service/scully-routes.service';
1513
import {fetchHttp} from '../utils/fetchHttp';
14+
import {findComments} from '../utils/findComments';
1615

16+
interface ScullyContent {
17+
html: string;
18+
cssId: string;
19+
}
20+
declare global {
21+
interface Window {
22+
scullyContent: ScullyContent;
23+
}
24+
}
1725
/** this is needed, because otherwise the CLI borks while building */
1826
const scullyBegin = '<!--scullyContent-begin-->';
1927
const scullyEnd = '<!--scullyContent-end-->';
@@ -36,22 +44,15 @@ const scullyEnd = '<!--scullyContent-end-->';
3644
preserveWhitespaces: true,
3745
})
3846
export class ScullyContentComponent implements OnInit, OnDestroy {
39-
@Input() type = 'MD';
40-
4147
elm = this.elmRef.nativeElement as HTMLElement;
42-
// mutationSubscription: Subscription;
48+
/** pull in all available routes into an eager promise */
4349
routes = this.srs.available$.pipe(take(1)).toPromise();
4450

45-
constructor(
46-
private elmRef: ElementRef,
47-
private srs: ScullyRoutesService,
48-
private router: Router,
49-
private idle: IdleMonitorService
50-
) {}
51+
constructor(private elmRef: ElementRef, private srs: ScullyRoutesService, private router: Router) {}
5152

5253
ngOnInit() {
53-
/** make sure the idle-check is loaded. */
54-
this.idle.init();
54+
// /** make sure the idle-check is loaded. */
55+
// this.idle.init();
5556
if (this.elm) {
5657
/** this will only fire in a browser environment */
5758
this.handlePage();
@@ -64,11 +65,16 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
6465
*/
6566
private async handlePage() {
6667
const template = document.createElement('template');
67-
// tslint:disable-next-line: no-string-literal
68-
if (window['scullyContent']) {
68+
const currentCssId = this.getCSSId(this.elm);
69+
if (window.scullyContent) {
6970
/** upgrade existing static content */
70-
// tslint:disable-next-line: no-string-literal
71-
template.innerHTML = window['scullyContent'];
71+
const htmlString = window.scullyContent.html;
72+
if (currentCssId !== window.scullyContent.cssId) {
73+
/** replace the angular cssId */
74+
template.innerHTML = htmlString.split(window.scullyContent.cssId).join(currentCssId);
75+
} else {
76+
template.innerHTML = htmlString;
77+
}
7278
} else {
7379
const curPage = location.href;
7480
/**
@@ -81,7 +87,12 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
8187
await fetchHttp(curPage, 'text')
8288
.then((html: string) => {
8389
try {
84-
template.innerHTML = html.split(scullyBegin)[1].split(scullyEnd)[0];
90+
const htmlString = html.split(scullyBegin)[1].split(scullyEnd)[0];
91+
if (htmlString.includes('_ngcontent')) {
92+
/** update the angular cssId */
93+
const atr = '_ngcontent' + htmlString.split('_ngcontent')[1].split('=')[0];
94+
template.innerHTML = htmlString.split(atr).join(currentCssId);
95+
}
8596
} catch (e) {
8697
template.innerHTML = `<h2 id="___scully-parsing-error___">Sorry, could not parse static page content</h2>
8798
<p>This might happen if you are not using the static generated pages.</p>`;
@@ -93,6 +104,7 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
93104
console.error('problem during loading static scully content', e);
94105
});
95106
}
107+
/** insert the whole thing just before the `<scully-content>` element */
96108
const parent = this.elm.parentElement || document.body;
97109
const begin = document.createComment('scullyContent-begin');
98110
const end = document.createComment('scullyContent-end');
@@ -130,7 +142,7 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
130142
return;
131143
}
132144
/** delete the content, as it is now out of date! */
133-
window['scullyContent'] = undefined;
145+
window.scullyContent = undefined;
134146
/** check for the same route with different "data", and NOT a level higher (length) */
135147
if (curSplit.every((part, i) => splitRoute[i] === part) && splitRoute.length > curSplit.length) {
136148
/**
@@ -155,59 +167,9 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
155167
}
156168
}
157169

158-
ngOnDestroy() {
159-
// if (this.mutationSubscription) {
160-
// this.mutationSubscription.unsubscribe();
161-
// }
170+
getCSSId(elm: HTMLElement) {
171+
return elm.getAttributeNames().find(a => a.startsWith('_ngcontent')) || 'none_found';
162172
}
163-
}
164173

165-
/**
166-
* Returns an observable that fires a mutation when the domMutationObserves does that.
167-
* if flattens the mutations to make handling easier, so you only get 1 mutationRecord at a time.
168-
* @param elm the elm to obse with a mutationObserver
169-
* @param config the config for the mutationobserver
170-
*/
171-
export function fromMutationObserver(
172-
elm: HTMLElement,
173-
config: MutationObserverInit
174-
): Observable<MutationRecord> {
175-
return new Observable(obs => {
176-
const observer = new MutationObserver(mutations => mutations.forEach(mutation => obs.next(mutation)));
177-
observer.observe(elm, config);
178-
return () => observer.disconnect();
179-
});
180-
}
181-
182-
/**
183-
* Returns an array of nodes coninting all the html comments in the element.
184-
* When a searchText is given this is narrowed down to only comments that contian this text
185-
* @param rootElem Element to search nto
186-
* @param searchText optional string that needs to be in a HTML comment
187-
*/
188-
function findComments(rootElem: HTMLElement, searchText?: string) {
189-
const comments = [];
190-
// Fourth argument, which is actually obsolete according to the DOM4 standard, seems required in IE 11
191-
const iterator = document.createNodeIterator(
192-
rootElem,
193-
NodeFilter.SHOW_COMMENT,
194-
{
195-
acceptNode: node => {
196-
// Logic to determine whether to accept, reject or skip node
197-
// In this case, only accept nodes that have content
198-
// that is containing our searchText, by rejecting any other nodes.
199-
if (searchText && node.nodeValue && !node.nodeValue.includes(searchText)) {
200-
return NodeFilter.FILTER_REJECT;
201-
}
202-
return NodeFilter.FILTER_ACCEPT;
203-
},
204-
}
205-
// , false // IE-11 support requires this parameter.
206-
);
207-
let curNode;
208-
// tslint:disable-next-line: no-conditional-assignment
209-
while ((curNode = iterator.nextNode())) {
210-
comments.push(curNode);
211-
}
212-
return comments;
174+
ngOnDestroy() {}
213175
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Returns an array of nodes coninting all the html comments in the element.
3+
* When a searchText is given this is narrowed down to only comments that contian this text
4+
* @param rootElem Element to search nto
5+
* @param searchText optional string that needs to be in a HTML comment
6+
*/
7+
export function findComments(rootElem: HTMLElement, searchText?: string) {
8+
const comments = [];
9+
// Fourth argument, which is actually obsolete according to the DOM4 standard, seems required in IE 11
10+
const iterator = document.createNodeIterator(
11+
rootElem,
12+
NodeFilter.SHOW_COMMENT,
13+
{
14+
acceptNode: node => {
15+
// Logic to determine whether to accept, reject or skip node
16+
// In this case, only accept nodes that have content
17+
// that is containing our searchText, by rejecting any other nodes.
18+
if (searchText && node.nodeValue && !node.nodeValue.includes(searchText)) {
19+
return NodeFilter.FILTER_REJECT;
20+
}
21+
return NodeFilter.FILTER_ACCEPT;
22+
},
23+
}
24+
// , false // IE-11 support requires this parameter.
25+
);
26+
let curNode;
27+
// tslint:disable-next-line: no-conditional-assignment
28+
while ((curNode = iterator.nextNode())) {
29+
comments.push(curNode);
30+
}
31+
return comments;
32+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {Observable} from 'rxjs';
2+
/**
3+
* Returns an observable that fires a mutation when the domMutationObserves does that.
4+
* if flattens the mutations to make handling easier, so you only get 1 mutationRecord at a time.
5+
* @param elm the elm to obse with a mutationObserver
6+
* @param config the config for the mutationobserver
7+
*/
8+
export function fromMutationObserver(
9+
elm: HTMLElement,
10+
config: MutationObserverInit
11+
): Observable<MutationRecord> {
12+
return new Observable(obs => {
13+
const observer = new MutationObserver(mutations => mutations.forEach(mutation => obs.next(mutation)));
14+
observer.observe(elm, config);
15+
return () => observer.disconnect();
16+
});
17+
}

schematics/scully/src/files/blog-module/blog.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, OnInit, ViewEncapsulation} from '@angular/core';
2-
import {ActivatedRoute, Router, ROUTES} from '@angular/router';
2+
import {ActivatedRoute, Router} from '@angular/router';
33

44
declare var ng: any;
55

scully.sampleBlog.config.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,32 +47,15 @@ exports.config = {
4747
property: 'id',
4848
},
4949
},
50-
'/todos/:todoId': {
51-
// Type is mandatory
52-
type: 'json',
53-
/**
54-
* Every parameter in the route must exist here
55-
*/
56-
todoId: {
57-
url: 'https://jsonplaceholder.typicode.com/todos',
58-
property: 'id',
59-
/**
60-
* Headers can be sent optionally
61-
*/
62-
headers: {
63-
'API-KEY': '0123456789',
64-
},
65-
},
66-
},
6750
'/blog/:slug': {
6851
type: 'contentFolder',
69-
postRenderers: ['toc'],
52+
// postRenderers: ['toc'],
7053
slug: {
7154
folder: './blog',
7255
},
7356
},
7457
'/**': {
75-
type: 'void',
58+
type: 'ignored',
7659
},
7760
},
7861
};

scully/renderPlugins/content-render-utils/getScript.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @returns a string representing the script that parses the page and loads the scullyContent variable.
33
* The string is kept on one line as the focus is to keep it as small as possible.
44
*/
5-
export function getScript(): string {
5+
export function getScript(attr): string {
66
// tslint:disable-next-line:no-unused-expression
7-
return `<script>try {window['scullyContent'] = document.body.innerHTML.split('<!--scullyContent-begin-->')[1].split('<!--scullyContent-end-->')[0];} catch(e) {console.error('scully could not parse content',e);}</script>`;
7+
return `<script>try {window['scullyContent'] = {cssId:"${attr}",html:document.body.innerHTML.split('<!--scullyContent-begin-->')[1].split('<!--scullyContent-end-->')[0]};} catch(e) {console.error('scully could not parse content');}</script>`;
88
}

scully/renderPlugins/contentRenderPlugin.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {getScript} from './content-render-utils/getScript';
55
import {handleFile} from './content-render-utils/handleFile';
66
import {insertContent} from './content-render-utils/insertContent';
77
import {readFileAndCheckPrePublishSlug} from './content-render-utils/readFileAndCheckPrePublishSlug';
8+
import {JSDOM} from 'jsdom';
9+
import {nodeModuleNameResolver} from 'typescript';
810

911
registerPlugin('render', 'contentFolder', contentRenderPlugin);
1012

@@ -18,8 +20,15 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) {
1820
const {meta, fileContent} = await readFileAndCheckPrePublishSlug(file, route);
1921
// TODO: create additional "routes" for every slug
2022
route.data = {...route.data, ...meta};
23+
const attr = getIdAttrName(
24+
html
25+
.split('<scully-content')[1]
26+
.split('>')[0]
27+
.trim()
28+
);
2129
const additionalHTML = await handleFile(extension, fileContent);
22-
return insertContent(scullyBegin, scullyEnd, html, additionalHTML, getScript());
30+
const htmlWithNgAttr = addNgIdAttribute(additionalHTML, attr);
31+
return insertContent(scullyBegin, scullyEnd, html, htmlWithNgAttr, getScript(attr));
2332
} catch (e) {
2433
logWarn(
2534
`Error, probably missing "${yellow('<scully-content>')}" or "${yellow(
@@ -28,3 +37,31 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) {
2837
);
2938
}
3039
}
40+
41+
function addNgIdAttribute(html: string, id: string): string {
42+
try {
43+
const dom = new JSDOM(html, {runScripts: 'outside-only'});
44+
const {window} = dom;
45+
const {document} = window;
46+
const walk = document.createTreeWalker(document.body as any);
47+
let cur = (walk.currentNode as any) as HTMLElement;
48+
while (cur) {
49+
if (cur.nodeType === 1) {
50+
cur.setAttribute(id, '');
51+
}
52+
cur = (walk.nextNode() as any) as HTMLElement;
53+
}
54+
return document.body.innerHTML;
55+
} catch (e) {
56+
console.error(e);
57+
}
58+
59+
return '';
60+
}
61+
62+
function getIdAttrName(attrs: string): string {
63+
return attrs
64+
.split(' ')
65+
.find((at: string) => at.trim().startsWith('_ngcontent'))
66+
.split('=')[0];
67+
}

0 commit comments

Comments
 (0)