How to add stylesheet dynamically in Angular 2? - javascript

Is there a way to add stylesheet url or <style></style> dynamically in Angular2 ?
For example, if my variable is isModalOpened is true, I would like to add some CSS to few elements outside my root component. Like the body or html.
It's possible to do it with the DOM or jQuery but I would like to do this with Angular 2.
Possible ?
Thanks

You can create a <style> tag dynamically like this:
ngOnInit() {
const css = 'a {color: pink;}';
const head = document.getElementsByTagName('head')[0];
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
head.appendChild(style);
}

I am not sure you can do it to body or html, but you can do it to root component.
Create a service injected to root component
Let the service have a state ( may be BehaviorSubject )
Access that service and change the state when isModalOpened is changed
In root component , you will be watching this and change component parameter values
Inside root component html , you can change class values based on the component param values
Update : Setting background color from an inner component .
app.component.css
.red{
background: red;
}
.white{
background: white;
}
.green{
background: green;
}
app.component.html
<div [ngClass]="backgroundColor" ></div>
app.component.ts
constructor(private statusService: StatusService) {
this.subscription = this.statusService.getColor()
.subscribe(color => { this.backgroundColor = color; });
}
status.service.ts
private color = new Subject<any>();
public setColor(newColor){
this.color.next(newColor);
}
public getColor(){
return this.color.asObservable();
}
child.component.ts
export class ChildComponent {
constructor(private statusService: StatusService) {}
setColor(color:string){
this.statusService.setColor(color);
}
}
So whenever we call setColor and pass a color variable such as 'red', 'green' or 'white' the background of root component changes accordingly.

Put all your html code in a custom directive - let's call it ngstyle...
Add your ngstyle to your page using the directive tags, in our case:
<ngstyle><ngstyle>
but let's also append the logic to your directive using ng-if so you can toggle it on or off...
<ngstyle ng-if="!isModalOpened"><ngstyle>
Now if your 'isModalOpened' is set to a scope in your controller like this:
$scope.isModalOpened = false; //or true, depends on what you need
...you can toggle it true or false many different ways which should toggle your directive on and off.

Related

Combine :host() with :has() - not possible?

I have a web component with a shadow DOM and a default slot.
I need to apply certain styling based on the presence or absence of specific a light DOM descendant. Please note that I don't need a specific workaround for this specific styling, it's just an example and in the real world the example is alot more complex.
I also cannot work with regular DOM CSS like x-y:has(div) since I need to apply styles to an element in the shadow DOM based on the presence of the div in the light DOM.
Please note that the code snippet only works in browsers that support constructable stylesheets (e.g. Safari won't).
const styleStr = `
:host {
display: block;
border: 3px dotted red;
}
:host(:has(div)) {
border-color: green;
}
`;
let css;
try {
css = new CSSStyleSheet;
css.replaceSync(styleStr);
} catch(e) { console.error(e) }
customElements.define('x-y', class extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'}).adoptedStyleSheets.push(css);
this.shadowRoot.append(document.createElement('slot'))
}
})
<x-y>no div - should have red border</x-y>
<x-y>
<div>div, should have green border</div>
</x-y>
I was trying to find if maybe :host() is not accepting :has(), but was unable to find anything on it, neither in the spec, nor on MDN or caniuse.
Does anyone have definitive knowledge/reference about this, and can point me to some documentation?
You want to style slotted content based on an element inside the slot
Since <slot> are reflected, (deep dive: ::slotted CSS selector for nested children in shadowDOM slot)
you need to style a <slot> in its container element.
If you want that logic to be done from inside the Component,
you could do it from the slotchange Event, which checks if a slotted element contains that DIV
Then creates a <style> element in the container element
Disclaimer: Provided code is a Proof of Concept, not production ready
<my-component>
Hello Web Component
</my-component>
<!-- <my-component> will add a STYLE element here -->
<my-component>
<!-- <my-component> will assign a unique ID to the DIV -->
<div>Web Component with a DIV in the slot</div>
</my-component>
<script>
customElements.define("my-component", class extends HTMLElement {
constructor() {
super().attachShadow({mode: "open"}).innerHTML = `<slot/>`;
let slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", (evt) => {
[...slot.assignedNodes()].forEach(el => {
if (el.nodeName == "DIV") {
el.id = "unique" + new Date() / 1;
// inject a <style> before! <my-component>
this.before( Object.assign( document.createElement("STYLE"), {
innerHTML : `#${el.id} { background:lightgreen } `
}));
}
});
});
}
})
</script>
PS. Don't dynamically add any content inside <my-component>, because that slotchange will fire again...

Remove all occurances of class in angular 2 +

I have just started coding in angular5 and I came across need of removing all class occurances on click event.
Something like below we have in Jquery
$('.m-active').removeClass('m-active');
I am looking for alternative of this in angular2 + (Typescript)
You could use document.querySelector all to remove the class - in the following - I have two divs - iniitally set to be red / green text, but using querySelectorAll - I am removing the red class from the divs.
function toggleRedClass() {
var redDivs = document.querySelectorAll('.red');
if (redDivs.length) {
for(i=0;i<redDivs.length;i++) {
redDivs[i].classList.remove('red');
redDivs[i].classList.add('black')
}
} else {
var blackDivs = document.querySelectorAll('.black');
for(i=0;i<blackDivs.length;i++) {
blackDivs[i].classList.remove('black')
blackDivs[i].classList.add('red')
}
}
}
.red {color:red}
.green {color:green}
<div class="red">test</div>
<div class="green">test1</div>
<button type="button" onclick="toggleRedClass()">Click to toggle the red class</button>
In Angular 2+ better use bindings instead of jQuery
<div [class.my-class]="isMyClass">div 1</div>
<div [class.my-class]="isMyClass">div 2</div>
<button (click)="isMyClass = !isMyClass">toggle</button>
export class MyComponent {
isMyClass:boolean = true;
}
You can create a directive like this :
https://plnkr.co/edit/eKokX0IrsIWIuY9ACUZ4?p=preview
#Directive({
selector: '[class]'
})
export class ClassDirective {
#Input('class') claz;
private _claz;
public set claz(claz){
this._claz = claz;
}
public get claz(){
return this._claz;
}
#HostBinding('class') get hostClass(){
return this.claz;
}
constructor(){
console.log('***');
}
ngOnInit(){
console.log('this.classz',this.claz);
setTimeout(()=>{
this.claz= this.claz.replace('milad','');
},2000)
}
}
I know it doesn't do exactly what you want, but the idea is to create a Directive which has a selector called class and then you have access to all the classes in your application (obviously this component should be declared in your modules).
Then you can do whatever you'd like inside that directive, you can use host binding to override the classes and whatnot.
You can create an event listener to some button, pass the event listener's call back to this directive and let it do whatever you want.

Add a class to expanded row using vanilla JS

In my angular application I am attempting a workaround because the ag-Grid api getRowClass() is not working at intended. All I need to do is add a css class to an expanded row and remove it when the row is collapsed.
The original method using ag-Grid api that does not work looks as follows:
setRowNode(params) {
this.gridOptions.getRowStyle = (params) => {
if(params.node.expanded) {
return { background: 'red' };
}
}
}
Ideally I would be able to selected the DOM and append a class to it. I tried this with some JQuery and it worked, but for obvious reasons I do not want to have JQuery in this app. I wrote something along these lines:
$('.ag-row[row="'+params.node.id+'"]',self.api.grid.gridPanel.eBodyContainer).addClass('ag-row-focus');
How would I fulfill this req using vanilla JS?
You can do it by creating a custom directive, like this one :
//row-focus.directive.ts
import { Directive, HostBinding, HostListener } from '#angular/core';
#Directive({
selector: '[appRowFocus]' // you can target classes, id etc.: '.grid-Holdings' for example
})
export class RowFocusDirective {
#HostBinding('class.ag-row-focus') isFocused = false;
#HostListener('click') toggleOpen() {
this.isFocused = !this.isFocused;
}
}
Import this directive on your module, and then attach the directive to your elements :
// your-component.component.ts :
<div class="row" appRowFocus>
These elements will toggle the ag-row-focus on click. You can add different #HostListener for other events.

Better way of manipulating the DOM in Angular2

What's the better way of manipulating the DOM to change the background of a specific div, rather than using document.getElementById('id').style.backgroundImage.
I'm trying to change backgrounds as I change my Url, but the only way I could think and easy is using document.getElementById()
changeBg() {
var urlPath = window.location.pathname.split('/');
switch (urlPath[4]) {
case "Refreshments%20North":
document.getElementById('homeBg').style.backgroundImage = "url('./assets/imgs/spur-2.jpg')";
break;
... more cases
default:
document.getElementById('homeBg').style.backgroundImage = "url('./assets/imgs/background.jpg')";
}
}
I also tried Renderer dependency but how do I target homeBg using this?
this.renderer.setElementStyle(this.elRef.nativeElement, 'background-image', "url(./assets/imgs/spur-2.jpg)");
Template -- is basically just a div
<nav></nav>
<div id="homeBg"></div>
Edit --
Moved my changeBg() to my sharedService
public changeBg() {
var urlPath = window.location.pathname.split('/');
switch (urlPath[4]) {
case "Refreshments%20North":
this.homeBg = this.sanitizer.bypassSecurityTrustStyle("url('./assets/imgs/spur-2.jpg')");
break;
default:
this.homeBg = this.sanitizer.bypassSecurityTrustStyle("url('./assets/imgs/background.jpg')");
}
}
Calling changeBg() service in my profile component
ngOnInit() {
this.sharedService.changeBg(); // is this correct?
}
Profile template -- like this gives me an error Cannot read property 'homeBg' of undefined
<div class="home" id="homeBg" [style.background-image]="changeBg?.homeBg"></div>
Change background with route.param.subscribe()
this.routeSub = this.route.params.subscribe(params => {
this.sharedService.changeBg();
}
Using binding and directives is the preferred way in Angular2 instead of imperative DOM manipulation:
<div [style.background-image]="myService.homeBg"
You need to sanitize the URL for Angular to accept it.
See In RC.1 some styles can't be added using binding syntax for more details.
changeBg() {
var urlPath = window.location.pathname.split('/');
switch (urlPath[4]) {
case "Refreshments%20North":
this.homeBg = this.sanitizer.bypassSecurityTrustStyle("url('./assets/imgs/spur-2.jpg')");
break;
... more cases
default:
this.homeBg = this.sanitizer.bypassSecurityTrustStyle( "url('./assets/imgs/background.jpg')");
}
}
See also How to add background-image using ngStyle (angular2)?
You can use template references and #ViewChild decorator:
template :
<div #myDiv id="homeBg"></div>
component :
class MyComponent implements AfterViewInit{
#ViewChild("myDiv")
elRef:ElementRef
ngAfterViewInit(){
this.renderer.setElementStyle(this.elRef.nativeElement, 'background-image', "url(./assets/imgs/spur-2.jpg)");
}
}

Create custom script for DOM Manipulation

I'm currently working on an Angular 2 Project where I have a menu that should be closable by a click on a button. Since this is not heavy at all, I would like to put it outside of Angular (without using a component for the menu).
But I'm not sure of how to do it, actually I've just put a simple javascript in my html header, but shouldn't I put it somewhere else?
Also, what the code should be? Using class, export something? Currently this is my code:
var toggleMenuButton = document.getElementById('open-close-sidebar');
var contentHolder = document.getElementById('main-content');
var menuHolder = document.getElementById('sidebar');
var menuIsVisible = true;
var updateVisibility = function() {
contentHolder.className = menuIsVisible ? "minimised" : "extended";
menuHolder.className = menuIsVisible ? "open" : "closed";
}
toggleMenuButton.addEventListener('click', function() {
menuIsVisible = !menuIsVisible;
updateVisibility();
});
Finally moved to something with MenuComponent and a service, but I'm still encountering an issue.
MenuService.ts
#Injectable()
export class MenuService {
isAvailable: boolean = true;
isOpen: boolean = true;
mainClass: string = "minimised";
sidebarClass: string = "open";
updateClassName() {
this.mainClass = this.isOpen ? "minimised" : "extended";
this.sidebarClass = this.isOpen ? "open" : "closed";
}
toggleMenu(newState: boolean = !this.isOpen) {
this.isOpen = newState;
this.updateClassName();
}
}
MenuComponent.ts
export class MenuComponent {
constructor(private _menuService: MenuService) { }
public isAvailable: boolean = this._menuService.isAvailable;
public sidebarClass: string = this._menuService.sidebarClass;
toggleMenu() {
this._menuService.toggleMenu();
}
}
MenuComponent.html
<div id="sidebar" [class]="sidebarClass" *ngIf="isAvailable">
...
<div id="open-close-sidebar"><a (click)="toggleMenu()"></a></div>
The action are rightly triggered, if I debug the value with console.log, the class name are right but it didn't change the value of the class. I thought the binding was automatic. And I still do not really understand how to change it. Do I have to use Emmit like AMagyar suggested?
The advantage of using angular2 above your own implementation, greatly outweigh the marginal benefit in performance you will get from using plane JavaSccript. I suggest not going on this path.
If you however do want to continue with this, you should export a function and import and call this function inside the ngAfterViewInit of your AppComponent. The exported function should add the click EventListener and (important) set the document.getElementById variables. Because your script possibly won't be able to find those elements yet when it's loaded.
But let me emphasise once more, that angular2 is optimised for exactly these tasks, and once you get more familiar with it, it will also be a lot easier to code it.
update
For inter component communication you should immediately think about a service. Just create a service which stores the menu state and add this to your global ngModule providers array. For instance:
export class MenuService {
public get menuOpen(): boolean {
return this._menuOpen;
}
private _menuOpen: boolean;
public openMenu() : void {
this._menuOpen = true;
}
public closeMenu() : void {
this._menuOpen = false;
}
public toggleMenu() : void {
this._menuOpen = !this._menuOpen;
}
}
You can then inject this service into your menu component and bind the classes open/closed and minimized/extended to the MenuService.menuOpen.
#Component({
selector : 'menu'
template : `
<button (click)="menuService.toggleMenu()">click</button>
<div id="open-close-sidebar" [class.open]="menuService.menuOpen"></div>
`
})
export class MenuComponent {
constructor(public menuService: MenuService){}
}
For other component you can use the same logic to see if the menu is open or closed
update #2
You have to use a getter to get the value from menuService. There is only one way binding:
export class MenuComponent {
constructor(private _menuService: MenuService) { }
public get isAvailable(): boolean {
return this._menuService.isAvailable;
}
public get sidebarClass(): string {
return this._menuService.sidebarClass;
}
toggleMenu() {
this._menuService.toggleMenu();
}
}
FYI, it's better practice to use [class.open] instead of a string class name. If you want to do it like that, it will only require minimal change in your current css.
The main reason of why I want to avoid using Angular component is the
fact that my manipulation should be done over all the website and not
just the "menu" component.
You can create many components in Angular 2, it's easy and very practical.
The action will change the class on my menu (located in my menu
component) and on my main content (located outside of the component).
I don't know how to do it, and I'm not sure that this is the best
way... Maybe by binding the service value directly... –
The main content can have a child that is the Menu itself.
Take a look in this link. There are many solutions, one of them is to "emit" the child changes to the parent.
If you need an example I can provide one quickly.

Categories

Resources