I have a Angular Pipe:
import {Pipe, PipeTransform} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import * as Remarkable from 'remarkable';
import * as toc from 'markdown-toc';
@Pipe({
name: 'MarkdownToc'
})
export class MarkdownTableOfContentsPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
public transform(markdown: string) {
const toc_opts = {
linkify: function(tok, text, slug, options) {
const regex = /(.+\b)(.*)$/
slug = slug.replace(regex, function(str, g1) { return g1; });
tok.content = `[${text}](#${slug})`;
return tok;
}
}
const toc_md = new Remarkable('commonmark')
.use(toc.plugin(toc_opts))
const md = new Remarkable('commonmark')
md.renderer.rules.link_open = function(tokens, idx, options /* env */) {
var title = tokens[idx].title ? (' title="' + Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(tokens[idx].title)) + '"') : '';
var target = options.linkTarget ? (' target="' + options.linkTarget + '"') : '';
return '<a href="/articles' + Remarkable.utils.escapeHtml(tokens[idx].href) + '"' + title + target + '>';
};
const toc_md_text = toc_md.render(markdown);
console.log(md.render(toc_md_text.content));
return this.sanitized.bypassSecurityTrustHtml(md.render(toc_md_text.content));
}
}
It generates a list of links (this a shortened list):
<ul>
<li><a href="/articles#introduction">Introduction</a></li>
<li><a href="/articles#downloads">Downloads</a></li>
</uL>
However, every link shows up was "file:///" + href which of course won't work. Is there some way to fix the hrefs to get it to work or some other way.
In my controller, I have this function:
private async _show() {
const db = await this.databaseService.get();
const id = this.route.snapshot.paramMap.get('id').split('-').join(' ');
const article$ = db.article
.findOne(toTitleCase(id))
.$;
this.sub = article$.subscribe(async article => {
this.article = article;
const attachment = await this.article.getAttachment('index.md');
this.text = this.markdownPipe.transform(await attachment.getStringData());
this.intermoduleservice.toc = this.markdownTocPipe.transform(await attachment.getStringData());
this.zone.run(() => {});
});
}
The InterModuleService
is a global service to push the TOC to my Side Nav menu where the TOC is being located. It seems when I push the TOC html to the Side Nav through this service, there is no rendering updates performed on the HTML. So [routerLink]
bindings or Angular specific code never gets updated properly.
So here's what I would do in your case. The href
links themselfs are only a workaround because you are probably setting html as a plain string, which angular cannot take hold of and therefore no angular specific code like routing can be injected like that. This is not ideal because it results in a full page reload and another full angular initialization, which ultimately takes a lot of time.
Let's see if we can make it faster by trying to implement native angular routing. Just to be clear, I don't know if this works for Electron. What you can do is refer to the html tag where you're injecting the markdown with @ViewChild()
. I'm talking about the last level of html that was created through an angular angular component. Let's say it would be a div
that has an innerHTML
attribute where you're using your pipe on.
<div [innerHTML]="originalMarkdown | MarkdownToc" #markdownDiv></div>
Now this way the inner HTML is completely inserted as a string without angular knowing what's inside, so no angular stuff works inside of this div. What you can do though is using the reference on that div to walk down the HTML tree of the innerHTML after it was created.
@ViewChild('markdownDiv') markdownDiv: ElementRef;
// this hook makes sure the html is already available
ngAfterViewInit() {
// find a link tag inside your innerHTML
// this can be any selector, like class, id or tag
var link = this.markdownDiv.nativeElement.querySelector("a");
// ... do something with the link
}
You said you have multiple links, maybe you have to use another selector or even give each link an ID in the html creation and refer to that here. As soon as you have a reference to the link you can use it to create an onClick function that can for example use routing. We need something called the Renderer2
here that helps us with it. It is injected as a normal service. Here's the majority of the code you need which can be placed in your component.ts
.
constructor(private renderer: Renderer2, private router: Router){}
@ViewChild('markdownDiv') markdownDiv: ElementRef;
// this hook makes sure the html is already available
ngAfterViewInit() {
// find a link tag inside your innerHTML
// this can be any selector, like class, id or tag
var link = this.markdownDiv.nativeElement.querySelector("a");
// ... on click
this.renderer.listen(link, 'click', (event) => {
this.router.navigateByUrl("/articles#introduction");
});
}
Now that's a lot to do but it may solve your problem with a faster implementation than you are trying to do it right now. If you have any questions, feel free to ask.
Edit
I suppose the life cycle hook ngAfterViewInit()
described here might just be too early to query your element, because in this stage the DOM has not been updated yet. If I see it correctly, you are making a database call to create your markdown, which takes time. Angular usually just inits the view of your template and does not rely on dynamic DOM manipulation afterwards.
What we have to do is probably hook into a later stage. This is a bit more tricky, but should still be managable. What we're gonna use is ngAfterViewChecked()
.
ngAfterViewChecked()
Respond after Angular checks the component's views and child views.
Called after the ngAfterViewInit and every subsequent
ngAfterContentChecked()
.
It's pretty similar to the ngAfterViewInit()
, we just have to make sure that we don't create the click listener multiple times on the same <a>
tag, because the function might be called often. So keep in mind, we only want to do it once. So we need to declare a boolean that states if we already have added a listener. Also we should check if the DOM is really there because the function is also called a few times before your HTML was injected. Here's the code.
constructor(private renderer: Renderer2, private router: Router){}
@ViewChild('markdownDiv') markdownDiv: ElementRef;
// this is our boolean that prevents us from multiple addings
listenerAdded: boolean = false;
// this hook makes sure the html is already available
ngAfterViewChecked() {
// check if the div we watch has some innerHTML and if we haven't set the link already
if(this.markdownDiv.nativeElement.childNodes.length>0 && !listenerAdded) {
// find a link tag inside your innerHTML
// this can be any selector, like class, id or tag
var link = this.markdownDiv.nativeElement.querySelector("a");
// ... on click
this.renderer.listen(link, 'click', (event) => {
this.router.navigateByUrl("/articles#introduction");
});
// remember not to do it again
this.listenerAdded=true;
}
}
Okay, so I added a click
event to the <div></div>
holding the TOC:
<mat-sidenav mode="side" #sidenav id="sidenav" fixedInViewport="fixed" fixedTopGap="65">
<button mat-menu-item [routerLink]="['articles']">
<mat-icon svgIcon="arrow-left"></mat-icon>
Return to Article List
</button>
<div class="sidenav-toc" (click)="onTocClick($event)" [innerHtml]="article | MarkdownToc" id="toc" #tocDiv></div>
</mat-sidenav>
Then on my sidenav component I added two functions:
public onTocClick(event: Event) {
const elem = event.target as Element;
if (elem.tagName.toLowerCase() == 'a') {
const frag = elem.getAttribute('data-link');
const id = this.interModuleService.currentArticle.split(' ').join('-');
this.goTo(frag);
}
}
goTo(anchor: string) {
// TODO - HACK: remove click once https://github.com/angular/angular/issues/6595 is fixed
(<HTMLScriptElement>document.querySelector('#'+ anchor)).scrollIntoView();
}
I had thought about some kind of dynamic component. However, due to the issue in Angular, it's not easy to scroll to an anchor either due to the issue above.