Skip to content

Commit

Permalink
Sander/css scoping (#250)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
SanderElias authored Jan 30, 2020
1 parent 4ae475e commit ce599ed
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"cSpell.words": ["Scully", "Scullyio"],
"cSpell.words": ["Scully", "Scullyio", "ngcontent"],
"peacock.affectActivityBar": true,
"peacock.affectTitleBar": true,
"peacock.affectStatusBar": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {map} from 'rxjs/operators';
})
export class BlogListComponent implements OnInit {
blogs$ = this.srs.available$.pipe(
map(routeList => routeList.filter((route: ScullyRoute) => route.route.startsWith(`/blog/`)))
map(routeList => routeList.filter((route: ScullyRoute) => route.route.startsWith(`/blog/`))),
map(blogs => blogs.sort((a, b) => (a.date < b.date ? -1 : 1)))
);

constructor(private srs: ScullyRoutesService) {}
Expand Down
2 changes: 1 addition & 1 deletion projects/sampleBlog/src/app/blog/blog.component.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
::slotted(h1) {
h1 {
color: rgb(51, 6, 37);
background-color: rgb(248, 211, 236);
padding: 5px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import {Router} from '@angular/router';
import {Observable, Subscription} from 'rxjs';
import {take} from 'rxjs/operators';
import {IdleMonitorService} from '../idleMonitor/idle-monitor.service';
import {ScullyRoutesService} from '../route-service/scully-routes.service';
import {fetchHttp} from '../utils/fetchHttp';
import {findComments} from '../utils/findComments';

interface ScullyContent {
html: string;
cssId: string;
}
declare global {
interface Window {
scullyContent: ScullyContent;
}
}
/** this is needed, because otherwise the CLI borks while building */
const scullyBegin = '<!--scullyContent-begin-->';
const scullyEnd = '<!--scullyContent-end-->';
Expand All @@ -36,22 +44,15 @@ const scullyEnd = '<!--scullyContent-end-->';
preserveWhitespaces: true,
})
export class ScullyContentComponent implements OnInit, OnDestroy {
@Input() type = 'MD';

elm = this.elmRef.nativeElement as HTMLElement;
// mutationSubscription: Subscription;
/** pull in all available routes into an eager promise */
routes = this.srs.available$.pipe(take(1)).toPromise();

constructor(
private elmRef: ElementRef,
private srs: ScullyRoutesService,
private router: Router,
private idle: IdleMonitorService
) {}
constructor(private elmRef: ElementRef, private srs: ScullyRoutesService, private router: Router) {}

ngOnInit() {
/** make sure the idle-check is loaded. */
this.idle.init();
// /** make sure the idle-check is loaded. */
// this.idle.init();
if (this.elm) {
/** this will only fire in a browser environment */
this.handlePage();
Expand All @@ -64,11 +65,16 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
*/
private async handlePage() {
const template = document.createElement('template');
// tslint:disable-next-line: no-string-literal
if (window['scullyContent']) {
const currentCssId = this.getCSSId(this.elm);
if (window.scullyContent) {
/** upgrade existing static content */
// tslint:disable-next-line: no-string-literal
template.innerHTML = window['scullyContent'];
const htmlString = window.scullyContent.html;
if (currentCssId !== window.scullyContent.cssId) {
/** replace the angular cssId */
template.innerHTML = htmlString.split(window.scullyContent.cssId).join(currentCssId);
} else {
template.innerHTML = htmlString;
}
} else {
const curPage = location.href;
/**
Expand All @@ -81,7 +87,12 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
await fetchHttp(curPage, 'text')
.then((html: string) => {
try {
template.innerHTML = html.split(scullyBegin)[1].split(scullyEnd)[0];
const htmlString = html.split(scullyBegin)[1].split(scullyEnd)[0];
if (htmlString.includes('_ngcontent')) {
/** update the angular cssId */
const atr = '_ngcontent' + htmlString.split('_ngcontent')[1].split('=')[0];
template.innerHTML = htmlString.split(atr).join(currentCssId);
}
} catch (e) {
template.innerHTML = `<h2 id="___scully-parsing-error___">Sorry, could not parse static page content</h2>
<p>This might happen if you are not using the static generated pages.</p>`;
Expand All @@ -93,6 +104,7 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
console.error('problem during loading static scully content', e);
});
}
/** insert the whole thing just before the `<scully-content>` element */
const parent = this.elm.parentElement || document.body;
const begin = document.createComment('scullyContent-begin');
const end = document.createComment('scullyContent-end');
Expand Down Expand Up @@ -130,7 +142,7 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
return;
}
/** delete the content, as it is now out of date! */
window['scullyContent'] = undefined;
window.scullyContent = undefined;
/** check for the same route with different "data", and NOT a level higher (length) */
if (curSplit.every((part, i) => splitRoute[i] === part) && splitRoute.length > curSplit.length) {
/**
Expand All @@ -155,59 +167,9 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
}
}

ngOnDestroy() {
// if (this.mutationSubscription) {
// this.mutationSubscription.unsubscribe();
// }
getCSSId(elm: HTMLElement) {
return elm.getAttributeNames().find(a => a.startsWith('_ngcontent')) || 'none_found';
}
}

/**
* Returns an observable that fires a mutation when the domMutationObserves does that.
* if flattens the mutations to make handling easier, so you only get 1 mutationRecord at a time.
* @param elm the elm to obse with a mutationObserver
* @param config the config for the mutationobserver
*/
export function fromMutationObserver(
elm: HTMLElement,
config: MutationObserverInit
): Observable<MutationRecord> {
return new Observable(obs => {
const observer = new MutationObserver(mutations => mutations.forEach(mutation => obs.next(mutation)));
observer.observe(elm, config);
return () => observer.disconnect();
});
}

/**
* Returns an array of nodes coninting all the html comments in the element.
* When a searchText is given this is narrowed down to only comments that contian this text
* @param rootElem Element to search nto
* @param searchText optional string that needs to be in a HTML comment
*/
function findComments(rootElem: HTMLElement, searchText?: string) {
const comments = [];
// Fourth argument, which is actually obsolete according to the DOM4 standard, seems required in IE 11
const iterator = document.createNodeIterator(
rootElem,
NodeFilter.SHOW_COMMENT,
{
acceptNode: node => {
// Logic to determine whether to accept, reject or skip node
// In this case, only accept nodes that have content
// that is containing our searchText, by rejecting any other nodes.
if (searchText && node.nodeValue && !node.nodeValue.includes(searchText)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
}
// , false // IE-11 support requires this parameter.
);
let curNode;
// tslint:disable-next-line: no-conditional-assignment
while ((curNode = iterator.nextNode())) {
comments.push(curNode);
}
return comments;
ngOnDestroy() {}
}
32 changes: 32 additions & 0 deletions projects/scullyio/ng-lib/src/lib/utils/findComments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Returns an array of nodes coninting all the html comments in the element.
* When a searchText is given this is narrowed down to only comments that contian this text
* @param rootElem Element to search nto
* @param searchText optional string that needs to be in a HTML comment
*/
export function findComments(rootElem: HTMLElement, searchText?: string) {
const comments = [];
// Fourth argument, which is actually obsolete according to the DOM4 standard, seems required in IE 11
const iterator = document.createNodeIterator(
rootElem,
NodeFilter.SHOW_COMMENT,
{
acceptNode: node => {
// Logic to determine whether to accept, reject or skip node
// In this case, only accept nodes that have content
// that is containing our searchText, by rejecting any other nodes.
if (searchText && node.nodeValue && !node.nodeValue.includes(searchText)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
}
// , false // IE-11 support requires this parameter.
);
let curNode;
// tslint:disable-next-line: no-conditional-assignment
while ((curNode = iterator.nextNode())) {
comments.push(curNode);
}
return comments;
}
17 changes: 17 additions & 0 deletions projects/scullyio/ng-lib/src/lib/utils/fromMutationObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Observable} from 'rxjs';
/**
* Returns an observable that fires a mutation when the domMutationObserves does that.
* if flattens the mutations to make handling easier, so you only get 1 mutationRecord at a time.
* @param elm the elm to obse with a mutationObserver
* @param config the config for the mutationobserver
*/
export function fromMutationObserver(
elm: HTMLElement,
config: MutationObserverInit
): Observable<MutationRecord> {
return new Observable(obs => {
const observer = new MutationObserver(mutations => mutations.forEach(mutation => obs.next(mutation)));
observer.observe(elm, config);
return () => observer.disconnect();
});
}
2 changes: 1 addition & 1 deletion schematics/scully/src/files/blog-module/blog.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component, OnInit, ViewEncapsulation} from '@angular/core';
import {ActivatedRoute, Router, ROUTES} from '@angular/router';
import {ActivatedRoute, Router} from '@angular/router';

declare var ng: any;

Expand Down
21 changes: 2 additions & 19 deletions scully.sampleBlog.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,32 +47,15 @@ exports.config = {
property: 'id',
},
},
'/todos/:todoId': {
// Type is mandatory
type: 'json',
/**
* Every parameter in the route must exist here
*/
todoId: {
url: 'https://jsonplaceholder.typicode.com/todos',
property: 'id',
/**
* Headers can be sent optionally
*/
headers: {
'API-KEY': '0123456789',
},
},
},
'/blog/:slug': {
type: 'contentFolder',
postRenderers: ['toc'],
// postRenderers: ['toc'],
slug: {
folder: './blog',
},
},
'/**': {
type: 'void',
type: 'ignored',
},
},
};
4 changes: 2 additions & 2 deletions scully/renderPlugins/content-render-utils/getScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @returns a string representing the script that parses the page and loads the scullyContent variable.
* The string is kept on one line as the focus is to keep it as small as possible.
*/
export function getScript(): string {
export function getScript(attr): string {
// tslint:disable-next-line:no-unused-expression
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>`;
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>`;
}
39 changes: 38 additions & 1 deletion scully/renderPlugins/contentRenderPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {getScript} from './content-render-utils/getScript';
import {handleFile} from './content-render-utils/handleFile';
import {insertContent} from './content-render-utils/insertContent';
import {readFileAndCheckPrePublishSlug} from './content-render-utils/readFileAndCheckPrePublishSlug';
import {JSDOM} from 'jsdom';
import {nodeModuleNameResolver} from 'typescript';

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

Expand All @@ -18,8 +20,15 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) {
const {meta, fileContent} = await readFileAndCheckPrePublishSlug(file, route);
// TODO: create additional "routes" for every slug
route.data = {...route.data, ...meta};
const attr = getIdAttrName(
html
.split('<scully-content')[1]
.split('>')[0]
.trim()
);
const additionalHTML = await handleFile(extension, fileContent);
return insertContent(scullyBegin, scullyEnd, html, additionalHTML, getScript());
const htmlWithNgAttr = addNgIdAttribute(additionalHTML, attr);
return insertContent(scullyBegin, scullyEnd, html, htmlWithNgAttr, getScript(attr));
} catch (e) {
logWarn(
`Error, probably missing "${yellow('<scully-content>')}" or "${yellow(
Expand All @@ -28,3 +37,31 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) {
);
}
}

function addNgIdAttribute(html: string, id: string): string {
try {
const dom = new JSDOM(html, {runScripts: 'outside-only'});
const {window} = dom;
const {document} = window;
const walk = document.createTreeWalker(document.body as any);
let cur = (walk.currentNode as any) as HTMLElement;
while (cur) {
if (cur.nodeType === 1) {
cur.setAttribute(id, '');
}
cur = (walk.nextNode() as any) as HTMLElement;
}
return document.body.innerHTML;
} catch (e) {
console.error(e);
}

return '';
}

function getIdAttrName(attrs: string): string {
return attrs
.split(' ')
.find((at: string) => at.trim().startsWith('_ngcontent'))
.split('=')[0];
}

0 comments on commit ce599ed

Please sign in to comment.