Angular 5 + Electron + Generated TOC - javascript

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>Introduction</li>
<li>Downloads</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.

Related

Update dom after component rendered the view, best practice in Angular2?

I'm seeking some advice how I should handle elements when working with Angular2.
I have stored some elements id's in the localstorage, and want to set a selected class name on some specific elements.
For now I'm using this way:
ngAfterViewChecked() {
// Check if seats has been selected before
var selectedSeats: elementInterface = JSON.parse(localStorage.getItem('SelectedSeats'));
if (selectedSeats != null) {
var outboundElement = document.getElementById(selectedSeats.outboundSelectedElement);
var returnElement = document.getElementById(selectedSeats.returnSelectedElement);
this.autoSelectSeats(outboundElement);
this.autoSelectSeats(returnElement);
}
}
Method:
private autoSelectSeats(element: Element) {
// Set selected class on element
element.classList.add('selected');
}
I see two issues here. The first is the ngAfterViewChecked hook that fires more than once after the view is created. Is there something I can do so it only fires once?
Second: Is there a better way to get the element when you know the id and set a class attribute on it after the content has loaded?
I can't seem to find the Angular2 way of doing it, besides using this hook.
Any idea? Also, please don't post Jquery posts, as I don't want to implement that when using Angular :)
How about adding a custom directive to each of your "seat" elements and let that directive add the CSS class?
In your template, the directive would be used as follows (I'm guessing, since you didn't show your template):
<div *ngFor="let seat of seats" [highlight]="seat.id">
...
</div>
You need to pass some information to the directive to identify which seat it is working on. It seems better to pass an id directly (e.g. seat.id) rather than to rely on HTML ids (although in your case they might be one and the same).
Now the code for the directive:
#Directive({
selector: '[highlight]'
})
export class HighlightDirective {
#Input() highlight: string; // This will contain a seat.id
constructor(el: ElementRef, ss: SeatService) {
const selectedSeats = ss.getSelectedSeats();
// If current seat found in selectedSeats, mark it as selected.
if (selectedSeats.indexOf(this.highlight) !== -1) {
this.el.nativeElement.classList.add('selected');
}
}
}
The reason I'm using an external service SeatService to get the data from localStorage is that Angular will create an instance of HighlightDirective for every match it finds in your template. You don't want to refetch the selected seats in every instance (the service lets you cache the seats and return the same data).
Angular way has pretty good documentation, classes are toggled using the following syntax: [class.selected]="selected"

Generate a raw HTML string from a component file and a view model

We have a template like this.
the-template.html
<template><div>${Foo}</div></template>
We want to do this with it.
some-file.ts
let htmlString = makeItHappen('the-template.html', { Foo = 'bar' });
console.info(htmlString); // <div>bar</div>
What is the equivalent of our makeItHappen function?
Ok so here's the gist: https://gist.run/?id=d57489d279b69090fb20938bce614d3a
Here's the code in case that goes missing (with comments):
import {bindable} from 'aurelia-framework';
import {ViewLocator,ViewSlot,ViewEngine,ViewCompileInstruction} from 'aurelia-templating';
import {inject, Container} from 'aurelia-dependency-injection';
#inject(Element,ViewLocator,ViewEngine,Container)
export class LoadViewCustomAttribute {
#bindable view;
#bindable viewModel;
constructor(element,vl,ve,container) {
this.element = element;
this.vl = vl;
this.ve = ve;
this.container = container;
}
attached() {
// Get a view strategy for this view - this will let Aurelia know how you want to locate and load the view
var view = this.vl.getViewStrategy(this.view);
// Create a view factory from the view strategy (this loads the view and compiles it)
view.loadViewFactory(this.ve, new ViewCompileInstruction()).then(vf => {
// Create a view from the factory, passing the container (you can create a child container at this point if you want - this is what Aurelia usually does for child views)
var result = vf.create(this.container);
// Bind the view to the VM - I've passed the current VM as the override context which allows Aurelia to do away with the $parent trick
result.bind(this.viewModel, this);
console.log(result); // for inspection
// Optional - create a viewslot and add the result to the DOM -
// at this point you have a view, you can just look at the DOM
// fragment in the view if you want to pull out the HTML. Bear in
// mind, that if you do add to the ViewSlot - since nodes can only
// belong to 1 parent, they will be removed from the fragment in
// the resulting view (don't let this confuse you when debugging
// since Chrome shows a LIVE view of an object if you console.log(it)!)
// var vs = new ViewSlot(this.element, true);
// vs.add(result);
// Since you can't just get a fragments HTML as a string, you have to
// create an element, add the fragment and then look at the elements innerHTML...
var div = document.createElement('div');
div.appendChild(result.fragment);
console.log(div.innerHTML);
});
}
}
That should do it - and the usage:
<template>
<require from="load-view"></require>
<section>
<div load-view="view.bind: 'view-to-load.html'; view-model.bind: { someData: 'test' }"></div>
</section>
</template>
And finally view-to-load.html
<template>
<div>
this is the template... ${someData}
</div>
</template>
Obviously, this doesn't have to be a custom attribute - you can just inject the bits and compile in a helper class or something like that (which can just return the raw HTML string).
This would make the equivalent of your makeItHappen function the attached method in the custom attribute. Of course you need all the deps so you need to at least have Aurelias dependency injection support to get hold of those.
Note: I'd suggest always using a ViewSlot if you plan on adding the content to the DOM (assuming you have an element that can act as the anchor) since that's the way Aurelia works and it will have more consistent results since ViewSlots know how to add/remove grandchildren gracefully
This may not be possible in the case that you have a 3rd party plugin that accepts strings as template input - but if possible look for extension points that work with DOM nodes instead.

Angular 2 routing-deprecated: How to detect CanActivate?

I have a component that is decorated with #CanActivate.
#Component({
// ...
})
#CanActivate(() => false)
export class UserManagementComponent {
// ...
}
In my navigation menu I'd like to disable or hide the link that navigates to this route. How would I go about doing this?
<a [routerLink]="['UserManagement']">User management</a>
PS: I'm still on the deprecated routing mechanism, not the rc1 version.
If you move the calculation of the #CanActivate(() => ...) return value to a service, then you can access it from your whole application. You can create a directive and add it to the routerLink that injects the service and disables the routerLink when the condition for the route returns false.
See http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel for how to use DI in #CanActivate.
See Angular 2, disable routerLink and Angular2, what is the correct way to disable an anchor element? for how to disable a routerLink
I believe having an injected service is a way to go, because if you ever need to route to your component from other additional components, it'll be much easier and you don't have to duplicate the reflection code.
That being said, I was curious on how to do the reflection indeed. So it appears that's supposed to be added to ES7 specification. For now, 'reflection-metadata' package pollyfills for it and that's what Angular2 uses internally too. Although 'Reflect' is already part of TypeScript library, I didn't find any meta-data reflection and seems it only support object reflections.
//This is a bit hacky, but it might help.
...
export class Test {
...
logType(target : any, key : string) {
//stop TypeScript from mapping this to TypeScript Reflect.
var Reflect = Reflect || {};
//You still get an error if you havn't added the Reflect.js script in the page
var t = Reflect.getMetadata("design:type", target, key);
console.log('key:' + key + ' type: ' + t.name);
}
...
}
Sources:
http://blog.wolksoftware.com/decorators-metadata-reflection-in-typescript-from-novice-to-expert-part-4
https://www.npmjs.com/package/reflect-metadata

Angular Binding to a function on the view results to infinite calls to the data service

I'm trying to bind a src attribute of images inside an ngFor directive that looks like this:
<div *ngFor="imageId of imageIds">
<img [attr.src]="imageSrc(imageId)" alt="{{imageId}}">
</div>
the imageSrc method inside the component looks like this:
imageSrc(imageId: string){
var hostUrl = "http://192.168.0.101:3000/";
var imageUrl = hostUrl+"images/"+imageId;
var imgSrc = "";
this._imageService.getImage(imageId)
.subscribe(image => {
imgSrc = hostUrl + image.url
});
return imgSrc;
}
The getImage function in the Injectable ImageService looks like this:
getImage(id: string) {
var _imageUrl = "http://192.168.0.101:3000/images/"+id;
return this.http.get(_imageUrl)
.map(res => <Image>res.json().data)
.catch(this.handleError)
}
The problem is, with only two imageIds the *ngFor directive renders the two list items as expected (but no images displayed) but the call to the data service doesn't stop, it gets the two images over and over on what appears to be an infinite loop until the application crashes. what I'm I doing wrong?
I don't think this is related to *ngFor. If you bind from the view to a function then this function is called every time Angular runs change detection which is by default on every event that happens on your page.
In devMode (in contrary to prodMode) change detection even runs twice for each event.
Store the result in a property and bind to that property instead or at least return a cached result from your function on subsequent calls if the input parameter (id:string) hasn't changed since the last call.
For example like shown in https://stackoverflow.com/a/36291681/217408

Angular 2 - How to clear an input with a local variable?

Following the guide from the Angular2 site I have this html:
<input #something (keyup)="doneTyping($event)">
<button (click)="add(something .value)">Add</button>
with this controller:
#Component({
selector: 'my-app',
appInjector: [SomeService]
})
#View({
templateUrl: 'index-angular',
directives:[NgFor]
})
class MyAppComponent {
name: string;
stuff: Array<string>;
constructor(someService: SomeService) {
this.name = 'Angular2Sample';
this.stuff= someService.getStuff();
}
add(st: string){
this.stuff.push(st);
}
doneTyping($event) {
if($event.which === 13) {
this.stuff.push($event.target.value);
$event.target.value = null;
}
}
}
When the user hits enter in the input, the doneTyping method clears the input with $event.target.value = null;.
However I can't come with a way of doing the same after pushing the button.
You can pass the input as a parameter in the button
<input #something (keyup)="doneTyping($event)">
<!-- Input as paramter -->
<button (click)="add(something)">Add</button>
And later in the add function
add(st: HTMLInputElement){
this.stuff.push(st.value);
st.value = null;
}
Also, you usually want to avoid interacting with DOM as much as possible. I just checked the Todo app example on the angular2 github and they also access the DOM element, but the last real commit is 2 months old.
If you use data binding you can have a cleaner code which would result in something like :
<input [value]="_newStuff" (keyup.enter)="addStuff()">
<button (click)="addStuff()">Add</button>
Then in your class you can just define the member _newStuff : string, that you can implement addStuff as follow :
addStuff() {
this.stuff.push(_newStuff);
this._newstuff = '';
}
In most cases you might want _newStuff to be a model object that works as an interface like this :
class Stuff {
id : any;
property : any;
otherProperty : any;
}
And then your _newStuff would be _newStuff : Stuff; and you could map it like this : <input [value]="_newStuff.property" (keyup.enter)="addStuff()">.
I know your sample code is very simple and you just try to get it to work, but I believe the data binding way is more in the logic of the framework, and the Form API basically gives tools such as Control, ControlGroup and FormBuilder that help you map your model on your view with Validators and such. It would be too cumbersome on something a bit larger to access the DOM everytime you need to change the view. In the end your example is almost raw javascript executed in an Angular2 context.
Coming back to your example, now imagine you have another event that triggers a new stuff to be added, say a double click or so, you'd need to add another method that handles this event, passing it again the HTMLInputElement, and do basically the same thing as you do on the keyup event handler, thus duplicating code again. With data binding your component owns the state of the view and you can therefore have one simple method that won't be affected by what kind of event triggered it. There you can do the test if the model is valid ( even though for this you'd use the Form API then ).
Anyways, I know this has been answered already, but I thought I would just help to improve the solution given my current understanding of it and how it could be applied to real cases.
You can use a one-direction bindings to access the value of the input. This is a very clear architecture; you don't have to pass DOM elements to the controller.
Template:
<!-- controller.value -> input.value binding-->
<input #myinput [value]=myValue>
<button (click)="done(myinput.value)">Add</button>
Controller:
myValue: string; // If you change this then the bound input.value will also change
// be sure to import ngOnInit from #angular/common
ngOnInit() {
this.myValue = "";
}
done(newValue) {
// ... processing newValue
this.myValue = ""; // Clear the input
}
Here's a good way to actually get your input objects to manipulate
Just need to import ViewChild from #angular/core
Template:
<input #something (keyup)="doneTyping($event)">
Class:
#ViewChild("something") something : HTMLInputElement;
doneTyping(e : KeyboardEvent) {
var text = this.something.value;
if (text != "") {
//DO SOME STUFF!!!!
}
}
Since version 2 of Angular is old now, and we developers are in need much more trend solutions and but we may look old topics in order to find solutions here, I felt I should mention an answer from another topic similar to this.
That answer works and solved my problem in Angular 7 with Type Script. Here its link
There are two ways:
1) I it is created with var or let or const, then it cannot be deleted at all.
Example:
var g_a = 1; //create with var, g_a is a variable
delete g_a; //return false
console.log(g_a); //g_a is still 1
2) If it is created without var, then it can be deleted.. as follows:
declaration:
selectedRow: any;
selectRowOfBankForCurrentCustomer(row: any) {
this.selectedRow = row; // 'row' object is assigned.
}
deleting:
resetData(){
delete this.selectedRow; // true
console.log(this.selectedRow) // this is error because the variable deleted, not its content!!
}
A quick way to do it:
<input #something (keyup)="doneTyping($event)">
<button (click)="add(something.value);something.value=''">Add</button>
A more Angular way to do it:
HTML:
<input [(ngModel)]="intermediateValue">
<button (click)="add()">Add</button>
Controller:
intermediateValue: string;
add() {
this.doStuff(this.intermediateValue);
this.intermediateValue= "";
}

Categories

Resources