Theme service that affects all components and modals <ng-template let-modal> in Angular 6+ - html

According to the post Toggle a class by a button from header component that affect main app component by Angular 6+
I created a theme service, it works fine for all components by toggling class 'dark-mode' in app.component.html, until I found out that all of my modals <ng-template let-modal> are under the <body>, of course these modals don't work.
So I turned to another solution from this answer: stackoverflow.com/a/59123790/6630524
I tried to inject DOCUMENT and Renderer2 into the header component and it works fine. But I see now the theme service is deprecated, so I put Renderer2 to the theme service (not in header component anymore), and it refused to work!
Could you help me to retain the theme service, and still be able to apply the theme to all the components (and these modals as well)?

It turned out no one answered for quite some time, so I decided to dig into the solution myself, see below FYI.
First, still keep using the theme service by putting Renderer2 into the theme.service.ts (previously set locally in header component):
#Injectable({
providedIn: 'root'
})
export class ThemeService {
public isDarkMode: boolean;
private renderer: Renderer2;
constructor(
#Inject(DOCUMENT) private document: Document,
rendererFactory: RendererFactory2
) { this.renderer = rendererFactory.createRenderer(null, null);
}
public toggleDarkMode() {
this.isDarkMode = !this.isDarkMode;
if (this.isDarkMode) {
this.renderer.addClass(this.document.body, 'dark-mode');
} else {
this.renderer.removeClass(this.document.body, 'dark-mode');
}
}
}
Second, you need to set a public variable in a constructor, here I choose the constructor of header.component.ts:
export class HeaderComponent implements OnInit {
/*
* Inject the theme service which will be called by our button (click).
* #param {ThemeService} themeService instance.
*/
constructor(public themeService: ThemeService) {}
ngOnInit() {}
}
Then put a button in the header.component.html to toggle your themes (Note: You can put the button anywhere in your project, along with the public variable defined in the corresponding abc.component.ts)
<button (click)="themeService.toggleDarkMode()"><i class="fa fa-moon"></i></button>
Now the public toggleDarkMode function is fired anytime the button clicked, then the modals and other components will reflect the change when theme service get updated by your defined dark-mode style, for example:
#lucky.dark-mode {
color: #a2b9c8;
background-color: #01263f!important;
}
And you might need this applied to all the <body> in index.html:
<body id="lucky">
<app-root></app-root>
</body>
Good luck and happy coding!

Related

Angular 10. How to navigate to a component's sections via a different component's anchor link tag?

app.component.html
<!--I wish to keep this structure like this, because menu.component has position sticky at top: 0-->
<app-home></app-home>
<app-menu></app-menu>
<app-about></app-about>
<app-projects></app-projects>
<app-contact></app-contact>
<app-footer></app-footer>
menu.component.html
<!--On anchor link click, navigate to corresponding component-->
<nav>
<a>Home</a>
<a>About</a>
<a>Projects</a>
<a>Contact</a>
</nav>
home.component.html, about.component.html, projects.component.html, contact.component.html
<!--Basic structure-->
<section>
<p>component works!</p>
</section>
A Link to the basic structure of the web-site
https://stackblitz.com/edit/angular-ivy-ufce4g
I would have shared a link to the actual web-site(my personal web-site), but i don't know if i am allowed.
One solution would be to create a shared service between you menu component and your app component.
It would work like this:
Dispatch a notification when a navigation item is clicked in you menu component;
Listen for notification in your app component. When a new notification is received, scroll to the corresponding component on your page.
Something like this:
#Injectable({
providedIn: 'root'
})
export class NavigationService {
private _navigation$ = new BehaviorSubject<string>('home');
navigation$ = this._navigation$.asObservable();
constructor() { }
updateNavigation(item) {
this._navigation$.next(item);
}
}
In your menu component, call the updateNavigation method of NavigationService whenever a navigation item is clicked.
#Component({
selector: 'app-menu',
// ...
})
export class MenuComponent {
constructor(private navigationService: NavigationService) {}
// Call this method whenever one of your navigation items is clicked
// in your template.
//
// navigationItem could, for example, be a string/enum corresponding to
// the component that must be scrolled into view
handleNavigationItemClick(navigationItem: string) {
this.navigationService.updateNavigation(navigationItem);
}
}
Finally, in your app component, listen for a navigation item update and scroll to the component.
#Component({
// ...
})
export class AppComponent implements OnInit {
constructor(private navigationService: NavigationService) {}
ngOnInit() {
this.navigationService.navigation$.pipe(
distinctUntilChanged(),
).subscribe(navigationItem => this.scrollIntoView(navigationItem))
}
private scrollIntoView(component) {
// Scrolling logic ...
}
}

Adding to component constructor in Angular makes the entire page return blank?

I am trying to add a basic MatDialog to my project. In the project I have 2 components, a header for the page and another called "CardBox", which basically just holds cardboxes of links to different websites.
When you click on the "i" icon, I would like to open a dialog box with more information.
See image below.
Initially, my understanding was that I just add a MatDialog field in the constructor of Cardbox component. Like so:
cardboxes.component.html
<mat-card id="CARDBOX">
<img class="info" src="path/image.jpg" alt="image" height=25px (click)="openDialog()"/>
</mat-card>
cardboxes.component.ts
#Component({
selector: 'app-cardbox',
templateUrl: './cardbox.component.html',
styleUrls: ['./cardbox.component.scss']
})
export class CardboxComponent implements OnInit {
constructor(private dialog: MatDialog) { }
ngOnInit(): void {}
openDialog() {
this.dialog.open(CardBoxComponent);
}
}
(I'm aware that this is calling its own component, and would just open the same thing again. I am just trying to get it to work first.)
app.component.html
<div id="bg">
<app-header></app-header>
<br>
<app-cardbox></app-cardbox>
</div>
However, in doing so, it removes EVERYTHING from the page except the background, including the header component. This is what it looks like when the program is run when there is SOMETHING in the constructor of Cardbox.
As you can see, having something in the constructor gets rid of everything on the page, which does not make sense to me as it removes the header, which is a completely separate component from the cardbox. I have tried everything to make it work but still it is not working.
Why is touching the constructor makes the entire project blank? Is there something I forgot to add to another file? And how can I add a MatDialog popup feature to the project in a way that works?
TLDR: When I put anything in the constructor of one of my components, the entire page disappears. How do I resolve this?
Still seeking answer to this :(
You are using it wrong.
I am surprised your app compiles when doing this.dialog.open(CardBoxComponent)
What you need to do is, first create your dialog component.
To make things simple you can create it in the same file as you CardBox component, but make sure you put it outside CardBox class:
cardboxes.component.ts
#Component({
selector: 'dialog-overview-example-dialog',
templateUrl: 'dialog-overview-example-dialog.html',
})
export class DialogOverviewExampleDialog {
constructor(
public dialogRef: MatDialogRef<DialogOverviewExampleDialog>,
// data is gonna be the data you pass to dialog when you open it from CardBox
#Inject(MAT_DIALOG_DATA) public data: DialogData) {}
onNoClick(): void {
this.dialogRef.close();
}
}
then you create a template for the dialog component:
dialog-overview-example-dialog.html
<h1 mat-dialog-title>more info</h1>
<div mat-dialog-content>
<p>{{data.info}}</p>
</div>
finally you add openDialog(myInfo) function to your ts file, inside CardBox component:
cardboxes.component.ts
openDialog(myInfo): void {
const dialogRef = this.dialog.open(DialogOverviewExampleDialog, {
width: '250px',
// data you pass to your dialog
data: {info: myInfo}
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
this.animal = result;
});
}
and add it to your template too:
cardboxes.component.ts
<mat-card id="CARDBOX">
<img class="info" src="path/image.jpg" alt="image" height=25px (click)="openDialog('info about first site')"/>
</mat-card>
in this example I pass the info as a text, but it can be an object too.
Here is a demo to make things easier for you: link

Angular 7 image backgroud for View composed of 2 or more components?

this is my html:
<app-navbar></app-navbar>
<router-outlet></router-outlet>
I want to set an image as background for the home view that is divided in 2 components (navbarComponent and homeComponent -selector ) but this will not be aplied in the others component (e.g contactComponent -supose to use other image, with the selectors app-navbar, and app-contact).
So how can I use an image as backgroud for the view composed by the app-navbar and app-home ???
Why not to add the background image as a CSS class for your <body> tag that you can define in the global styles.css file and then under the page/route y'd like this to show up, add that class name to the <body> tag using one of these two options.
The first option is the easiest but less recommended:
constructor(#Inject(DOCUMENT) private document: Document) {}
ngOnInit(){
this.document.body.classList.add('test');
}
The second option (my personal recommendation) is to implement the custom rendering class from the #angular/core package:
import { Component, OnDestroy, Renderer2 } from '#angular/core';
export class myCoolComponent implements OnDestroy {
constructor(private renderer: Renderer2) {
this.renderer.addClass(document.body, 'test');
}
ngOnDestroy() {
this.renderer.removeClass(document.body, 'test');
}
Hope this will help you!

Angular event won't fire when clicking on svg

I have an angular (4) based web app, with an #angular/material based card view. Embedded in the content of the card view is a sub-component, which just displays an svg as an <object>.
Here is what that card looks like:
<md-card (click)="onSelect(line)">
<md-card-content (click)="onSelect(line)">
<app-line-overview [line]="line"></app-line-overview>
</md-card-content>
<md-card-footer>
<h2>{{line.name}}</h2>
<h3>OEE: {{line.oee}}</h3>
</md-card-footer>
</md-card>
The issue is that the (click) event doesn't work if I click on the svg image (presumably because it is on top of the card view?), but if I click around the svg, the event fires.
I tried the adding md-card { z-index: 999 } to the css, but it makes no difference. How can I ensure that clicking anywhere within the card fires the event regardless of what is inside it?
Maybe writing a directive with a listener on clicks would help :
#Directive({
selector: '[clickInside]'
})
export class ClickInsideDirective {
constructor(private elementRef: ElementRef) {
}
#Output()
public clickInside = new EventEmitter<Event>();
#HostListener('click', ['$event', '$event.target'])
public onClick(event: MouseEvent, targetElement: HTMLElement): void {
if (!targetElement) {
return;
}
const clickedInside = this.elementRef.nativeElement.contains(targetElement);
if (clickedInside) {
this.clickOutside.emit(event);
}
}
...
}
usage
<md-card (clickInside)="onSelect(line)">

Styled HTML content dynamically switched with tabs using Angular 2

I am attempting to create a reusable angular2 component that accepts an array of URLs to html files on my server and creates a content window with tabs to switch between "chapters", effectively swapping out the html and css inside the content window. I have tried all sorts of things including iframes but those don't work, the angular 1 ng-include work-arounds that I can find on StackOverflow but they have all since been deprecated, and the closest I've got is building a component that you can #Input html and it interpolates the content but style won't apply and angular strips out any style or script tags. Here is what I have tried.
In my parent component class:
htmlInput: string = "<h1>Why Does Angular make this so hard?</h1>";
cssInput: string = "h1 { color:red; }"
Parent Component HTML:
<app-html [html]='htmlInput' [css]='cssInput'></app-html>
My HTML Component:
import { Component, Input, OnInit } from '#angular/core';
#Component({
selector: 'app-html',
template: '<div [innerHtml]=html></div>', //This works but no style
//template: '{{html}}', //This displays the actual markup on page
styles: ['{{css}}'] //This does nothing
//styles: ['h1 { color: red; }']//Also nothing
})
export class HtmlComponent implements OnInit {
#Input() html: string = "";
#Input() css: string = "";
ngOnInit() {
}
}
The result of this code is
Why Does Angular make this so hard?
But no red color. Maybe style is applied before the innerHtml is added to DOM? I don't know but just putting {{html}} results in displaying the actual markup with the h1 tags visible.
The reason I want to do it this way is that I have a bunch of HTML pages already created sitting in a folder on my server from before I angularized my site that all share a single style sheet. I'd like to just be able to flip through them like pages in a book without reloading the page and since there are so many and I'm likely to add more all the time, I'd really rather not create routing for every single one. (I already have routing for basic site navigation.)
Does anybody have a better suggestion for how to embed styled HTML into a page dynamically in the most recent version of Angular 2? At the time of this post we are in 2.0.0-beta.17.
OR... I already figured I may be approaching this issue from the entirely wrong angle. There must be a reason Angular is making this so difficult and deprecating all the solutions people have come up with so If anyone has a suggestion about how I could achieve the same results in a more angular friendly way I'd love to hear that too.
Thank you.
Edit:
I was able to fix my issue by creating a pipe which sanatizes the html before adding it to an iframe.
import { Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
#Pipe({ name: 'safe' })
export class SafePipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(url: string) {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}
And then you can just pass your html into the iframe.
<iframe width="100%" height="1000" frameBorder="0" [src]="url | safe"></iframe>
This is useful to me since I have some old pages that use all sorts of jquery and style etc. This works as a quick fix to have them show up.
Angular2 rewrites the styles added to a component by including the dynamically added attributes like _ngcontent-yle-18 into the CSS selectors.
Angular2 uses this to emulate shadow DOM style encapsulation. These attributes are not added to dynamically added HTML (for example with innerHTML).
Workarounds
add styles to index.html because these styles are not rewritten by Angular2
set ViewEncapsulation.None because then Angular doesn't add the encapsulation emulation attributes
use /deep/ to make Angular2 ignore the encapsulation emulation attributes
See also Angular 2 - innerHTML styling
You should wrap your css into an object and use ngStyle to bind it to your component rather than the styles attribute, because styles does not support data binding.
Example:
htmlInput: string = "<h1>Why Does Angular make this so hard?</h1>";
cssInput: string = "{h1 { color:red; }}"
Parent Component HTML:
<app-html [html]='htmlInput' [css]='cssInput'></app-html>
Your HTML Component:
import { Component, Input, OnInit } from '#angular/core';
#Component({
selector: 'app-html',
template: '<div [innerHtml]="html" [ngStyle]="css"></div>',
styles: []
})
export class HtmlComponent implements OnInit {
#Input() html: string = "";
#Input() css: string = "";
ngOnInit() {
}
}