I have an Angular 6 app with a <div> that gets populated via an [innerHTML] binding. How can I apply target='_blank' to all the links inside this div?
What I've tried:
So far, I've tried creating a directive that wraps the div and, after change detection gets run, pulls up a list of child <a> tags and applies a target='_blank' attribute. So far, I have not been able to get ContentChildren to access any of the links: It just pulls up an empty list.
Does anyone have experience doing this, or is there a more elegant solution?
#Directive({
selector: '[appExternalLink]'
})
export class ExternalLinkDirective implements AfterContentChecked, AfterViewChecked {
#ContentChildren('a', {descendants: true}) links: QueryList<any>;
#Input() appExternalLink: string;
constructor() {
console.log('HELLO FROM APPEXTERNALLINK');
}
ngAfterContentChecked() {
console.log(this.links);
}
}
Then, when binding the content:
<div appExternalLink>
<div [innerHTML]="content"></div>
</div>
It turns out I was using the wrong approach trying to use ContentChildren.
With the below code, I was able to target all <a> tags in a div with the appExternalLink directive. Posting my solution so other people don't have to figure this out themselves:
import { AfterViewChecked, Directive, ElementRef } from '#angular/core';
#Directive({
selector: '[appExternalLink]'
})
export class ExternalLinkDirective implements AfterViewChecked {
constructor(private el: ElementRef) { }
ngAfterViewChecked() {
Array.from(this.el.nativeElement.querySelectorAll('a'))
.forEach((el: any) => {
el.setAttribute('target', '_blank');
});
}
}
You can use the <base> tag to default the target attribute of each anchor.
<base target="_blank">
You can inject this tag dynamically in ngOnInit and remove it in ngOnDestroy, but it will change the behaviour of any link.
If you want to change the behaviour of just the anchors inside a div, you can use
Array.from(document.querySelectorAll('.your-div-class a'))
.forEach(el => el.setAttribute('target', '_blank'))
This is the selector you're using
#ContentChildren('a', {descendants: true}) links: QueryList<any>;
What this does is look for any direct children of the component which have an id of a.
If you change your html to look like this:
<div appExternalLink>
<a #a>Link</>
<a #a>Link</>
</div>
Then your selector will find the links.
Related
Let's say I have some code like this:
class MyElem extends HTMLElement {
constructor() {
super();
let templateContent = document.getElementById('template-elem').content;
this.innerHTML = templateContent.cloneNode(true);
}
}
window.customElements.define('my-elem', MyElem);
<template id="template-elem">
<div class="a">
<div class="b">b</div>
<div class="c">c</div>
</div>
</template>
<my-elem></my-elem>
Why doesn't this work? In the Chrome inspector, the custom element has no HTML inside of it. I've also tried doing:
this.append(templateContent.cloneNode(true));
but that also resulted in an empty HTML tree.
All the tutorials mention using the shadow DOM like the following:
this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
and while that works, it forces you to use the Shadow DOM for your custom element. Is there no way of just appending the template's HTML to your custom element without being required to use the Shadow DOM? I'd prefer to just use global CSS styling in my small use-case.
You are falling into multiple traps, like everyone in their first Component adventures.
Custom Elements (strictly speaking only Elements with shadowDOM are Web Components) have lifecycle phases and Callbacks.
This diagram: https://andyogo.github.io/custom-element-reactions-diagram/ is a MUST to understand.
You want to add DOM content in the constructor phase; but there is no DOM Element yet in this phase.
Only in the connectedCallback can DOM content be added.
With shadowDOM this is another story, its "DocumentFragment" is available in the constructor, you can set content, But it is not a DOM Element yet! The connectedCallback tells you when your Custom Element was attached to the DOM.
Templates content is a DocumentFragment, but your .innerHTML expects a string.
Since (in your usage) <template> is a DOM element, you can read its innerHTML (see below)
So, yes Custom Elements without shadowDOM are possible:
You will see the <template> content twice, demonstrating the 2 ways of adding content.
<script>
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
let template = document.getElementById(this.nodeName);
this.innerHTML = template.innerHTML;
this.append(template.content.cloneNode(true))
}
})
</script>
<template id="MY-ELEMENT">
Hello, I am an Element!
</template>
<my-element></my-element>
The constructor is where you prepare your Element
This constructor also runs when you do document.createElement("my-element").
The connectedCallback runs when your Element is added to the DOM
If you do not specify a method, the method from its Class parent runs, so in the above code the (default) constructor from HTMLElement is executed.
That is why you need super() in your own constructor... to execute the constructor from HTMLElement.
Note:
constructor(){
let template = document.getElementById("MY-ELEMENT").content.cloneNode(true);
super().attachShadow({mode:"open").append(template);
}
is totally valid code; Google Documentation that says "super needs to run first" is wrong.
You need to run super() before you can access the Elements own scope with this
That is why I prefer:
constructor(){
// do anything you want here, but you can not use 'this'
super() // Sets AND Returns 'this'
.attachShadow({mode:"open") // both Sets AND Returns this.shadowRoot
.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
Note append() was not available in IE; so oldskool programmers won't know about its versatility: https://developer.mozilla.org/en-US/docs/Web/API/Element/append
When your Component adventures are going to involve Class inheritance;
you call parent methods with:
connectedCallback(){
super.connectedCallback()
}
The most simple implementation of a custom element would be:
class MyComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `<div>Hello world</div>`
}
}
customElements.define('my-component', MyComponent)
my-component {
display: block;
border: 1px dotted #900
}
<my-component></my-component>
However, if you don’t use Shadow DOM, you cannot encapsulate the CSS, but have to style the component through an external style sheet.
The most simple way to write a component with Shadow DOM would look like this:
class MyOtherComponent extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({ mode: "open" })
}
connectedCallback() {
this.shadow.innerHTML = `
<style>
:host {
display: block;
border: 1px dotted #900
}
</style>
<div class="component">Hello World!</div>
`
}
}
customElements.define('my-other-component', MyOtherComponent)
<my-other-component></my-other-component>
This way, you have a bit more overhead, but the component is truly encapsulated.
I am trying to bind a dictionary of attributes like {'id': 'myid', 'class': 'my-1 pr-md-2', ...} at the time I am defining the element. I do not want to set that attributes after the DOM is loaded by Javascript.
I am struggling with the correct form of doing this. I can not bind them one by one declaring the attribute name and value manually as they are user given parameters. I have think of appending them to the attributes property, but I do not know if it is advisable to do it this way.
This is my data structure:
<template is="dom-repeat" items="{{links}}" as="link">
<a class="nav-item nav-link mr-md-2" on-tap="changePage">{{link.title}}</a>
</template>
and the attributes are saved in each link.attributes property. My solution would be something like this:
HTML
<template is="dom-repeat" items="{{links}}" as="link">
<a attributes={{appendAttributes(link)}} class="nav-item nav-link mr-md-2" on-tap="changePage">{{link.title}}</a>
</template>
JS
appendAttributes: function(link){
//Get current attributes of the element and append the ones in link.attributes
}
Is this the correct way to handle it?
As far as I know this is not possible with Polymer's templating system: there's no way to access the element to which a computed binding is applied to.
This
<a attributes={{appendAttributes(link)}}></a>
can't work because the attributes property is read-only.
I can not bind them one by one declaring the attribute name and value manually as they are user given parameters
Actually if you know in advance what attributes/properties have to be set you can still set them dynamically:
<a id=[[userGivenId]]
class$=[[userGivenClass]]
...
></a>
Anyway, there is a lit-html directive made by open-wc called spread which does just what you want. This would require rewriting your component using LitElement to something like this:
import { LitElement, html, property, customElement } from 'lit-element';
import { repeat } from 'lit-html/directives/repeat';
import { spread } from '#open-wc/lit-helpers';
#customElement('my-element')
export class MyElement extends LitElement {
#property() userGivenId;
#property() links;
// ...
render() {
return html`
${repeat(this.links, link => html`
<a ...=${spread({
id: this.userGivenId,
'?my-boolean-attribute': true
'.myProperty': { foo: 'bar' },
'#my-event': () => console.log('my-event fired'),
})}
class="nav-item nav-link mr-md-2"
#click=${e => this.changePage(e)}
>${link.title}</a>
`)}
`;
}
}
With some limitations, PolymerElements and LitElements can coexist in the same project so converting a single component shouldn't cause any trouble.
I am using Angular Material's Tab. And i am suppose to add and remove class using:
const classotherClustersSelection = document.getElementsByClassName('others');
Array.from(classotherClustersSelection).forEach(item => {
item.classList.remove('others');
})
On change of a particular data i am suppose to remove class from the tabs section, from all tabs. But unfortunately other tab that contains 'others' class is hidden with ng-if, so i am not able to manipulate the DOM directly. Whats are the approaches possible?
The issue is that *ngIf not hide the element, it removes it completle from the dom.
That's why you are not able to access the element.
If you want to hide something but still access it, you should remove the element by style.
You can use class or style binding for that:
style binding:
<div [style.display]="conditionForHide ? 'none' : 'initial'">
class binding:
<div [class.removeClass]="conditionForHide">
For the second option you need to create the class in you style sheet. An example to remove the element without display:
.conditionForHide {
opacity: 0;
pointer-events: none;
cursor: default;
width: 0px;
height: 0px;
position: absolute;
}
Edit
You should not manipulate the material code. And angular offers you a couple of options to manipulate your code next to plane javascript (getElementsByClassName).
The fastest way is when the data change is triggered, you manipulate the class via class binding:
Controller:
public showClassOthers = true;
onDataChange() {
// do some tasks
this.showClassOthers = false;
}
HTML:
<mat-tab-group>
<mat-tab label="First" [class.others]="showClassOthers"> Content 1 </mat-tab>
<mat-tab label="Second" [class.others]="showClassOthers"> Content 2 </mat-tab>
</mat-tab-group>
Add ngClass property to your div like I did below:
<div [ngClass]="{others: boolClass}" *ngIf="data==='requiredValue'"></div>
Then change the value of boolClass variable where the data gets changed:
this.data = 'someOtherValue';
this.boolCass = (this.boolClass)?(this.data==='requiredValue'): false;
Now the class will be removed from the element.
You can refer from my example here https://stackblitz.com/edit/angular-cc6tru
You can use ngClass to Conditionally add/remove the class from the DOM element.
demo
In Demo, I am changing the data on button click and based on the value class is added or removed.
Can anyone help me understand how to control the ordering of a component root element css class and any css class that may be bound from the parent calling the component?
Here is a fiddle that depicts what I'm noticing (snippet example below):
https://jsfiddle.net/cicsolutions/b6rnaw25/
You'll notice if you have a component with a class on its root element, if that class is a string, Vue's class binding places the class at the beginning of the resulting bound class list. This is what I would expect because because the component sets the base css class and then you can customize the styles when you use the component by adding classes to the component html element. Then Vue binds/joins the classes together.
In the next examples in the fiddle, I'm showing the use of a css class that is dynamic (i.e. not a static string). In these cases, Vue places the component's root element class at the end of the bound class list.
I'm working on a component that I hope others will use, so I'd like to set my component class on the root element, and then if anyone wants to override those styles, they can just add their own class on the component tag.
I also need the root element class to be dynamic, so I must use an array or an object to handle the class binding.
Does anyone know why Vue places the component root css class at the beginning for static classes and at the end for dynamic classes? That seems strange to me, but perhaps it's intentional for a reason that eludes me.
None the less, how would I go about ensuring that my component's root element class is always first in the resulting bound class list, when I need it to be a dynamic class?
Vue.directive('bound-class', (el) => {
const boundClass = el.attributes.class.nodeValue
const boundClassPrintout = document.createElement('div')
boundClassPrintout.innerHTML = 'Resulting Bound Class: ' + boundClass
el.appendChild(boundClassPrintout)
});
// STATIC CSS CLASS -> becomes 1st class in bound class list (expected)
Vue.component('string-test', {
template: `<div class="string-class" v-bound-class><slot></slot></div>`
});
// DYNAMIC CSS CLASS -> becomes last class in bound class list (unexpected)
Vue.component('array-test', {
template: `<div :class="['array-class']" v-bound-class><slot></slot></div>`
});
// DYNAMIC CSS CLASS -> becomes last class in bound class list (unexpected)
Vue.component('object-test', {
template: `<div :class="{ 'object-class': true }" v-bound-class><slot></slot></div>`
});
new Vue({
el: "#app",
computed: {
vueVersion() {
return Vue.version
}
}
})
body {
background: #20262E;
padding: 20px;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
}
h2 {
margin-bottom: 0.75rem;
}
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Vue version: {{ vueVersion }}</h2>
<string-test class="p-2 mb-2 border">Root class (string-class) at beginning (expected)</string-test>
<array-test class="p-2 mb-2 border">Root class (array-class) at end (unexpected)</array-test>
<object-test class="p-2 mb-2 border">Root class (object-class) at end (unexpected)</object-test>
</div>
I suspect that there's no particular reason why Vue inserts static classes first; possibly it's just mirroring the order of the input parameters in the renderClass function.
Also the order of rule sets in CSS files matters; the order of class names in the class attribute of elements does not. And neither order has anything to do with the cascade, which refers to child elements inheriting styles from their parents. Perhaps you've confused that with the order of declarations within a block or within an inline style. In that case order does matter:
<p class="red blue">
Order doesn't matter in the class attribute above. If
the class styles contradict, whichever is defined last
will win regardless of how they're ordered in the attribute.
</p>
<p class="blue red">
This paragraph will be styled identically to the previous
one, despite the change in class order.
</p>
<p style="color: red; color: blue">
Order does matter here. The text color will be blue.
</p>
I have implemented a 'tree' list of categories for an Angular2 (Typescript) app I am developing. This component is supposed to allow you to be able to click on a category name (no matter whether it's a category or sub-category) and this will show products of the category.
My 'category-tree' component is a separate component and it is used recursively so I can traverse the category hierarchy correctly. For each category a span is generated with a 'click' event binded to it. When clicked I use the emit function to broadcast this information back to the parent component in order to update some variables there.
This functionality is working for top-level categories but the click is not working correctly when it is on a child category. The
function which watches for the change does not receive any information.
Here is my code:
The function which logs out the information into my console. This is on the parent component:
changeCategory(event) {
console.log(event);
}
The html for the parent which holds the directive tag and the emit event name (categoryChange):
<div id='left-menu-wrapper'>
<div id='left-menu'>
<h1>{{title}}</h1>
<h2>Categories</h2>
<ul class="categories">
<category-tree [categories]="categories" (categoryChange)="changeCategory($event)"></category-tree>
</ul>
<div *ngIf="selectedCategory">
{{selectedCategory.name}}
</div>
</div>
<div *ngIf="!contentLoaded" class='spinner'></div>
</div>
<product-view [product]="selectedProduct"></product-view>
The child component:
import { Component, Input, Output, EventEmitter, forwardRef } from 'angular2/core';
#Component({
selector: 'category-tree',
templateUrl: './app/views/category-tree.html',
directives: [forwardRef(() => CategoryTree)],
outputs: ['categoryChange']
})
export class CategoryTree {
#Input() categories;
public categoryChange:EventEmitter;
constructor() {
this.categoryChange =new EventEmitter();
}
categoryClick(category) {
this.categoryChange.emit({
value: category
});
}
}
And the recursive component html:
<li *ngFor="#category of categories">
<span (click)="categoryClick(category)" [class.selected]="category === selectedCategory">{{category.name}}</span>
<ul *ngIf="category.sub_categories" class='sub-category'>
<category-tree [categories]="category.sub_categories"></category-tree>
</ul>
</li>
As you can see, I bind a click event to each category which is that current category iteration. This calls an emit function in the category-tree class with that information and broadcasts it back. Again this works with a parent category but not a child.
My thinking is that as a child's direct parent component isn't the app.component.ts this may be causing an issue? I'm not sure.
Any ideas?
Thanks
The problem here is that the emit can only talk directly to it's parent component.
Because of this, I found a very useful question and answer here which explained Service events and how to communicate with deep-level components using a service like this:
Global Events in Angular 2