How to get a DOM element in Stenciljs? - javascript

import { Component, Prop } from '#stencil/core';
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Prop() first: string;
#Prop() last: string;
getElementHere() {
// how can I get the div here?
}
render() {
return (
<div>
Hello, World! I'm {this.first} {this.last}
</div>
);
}
}
I want to get the DOM element just like in native JS. How do you do this in Stencil? getElementById does not work.

To expand on Fernando's answer, the #Element decorator binds the component's root element to this property. It's important to note a few properties of this approach:
The #Element bound property is only available after the component has been loaded (componentDidLoad).
Because the element is a standard HTMLElement, you can access elements within your current component using the standard .querySelector(...) or .querySelectorAll(...) methods to retrieve and manipulate them.
Here is an example showing when the element is accessible, and how to manipulate nodes within this element (correct as of stencil 0.7.24):
import { Component, Element } from '#stencil/core';
#Component({
tag: 'my-component'
})
export class MyComponent {
#Element() private element: HTMLElement;
private data: string[];
constructor() {
this.data = ['one', 'two', 'three', 'four'];
console.log(this.element); // outputs undefined
}
// child elements will only exist once the component has finished loading
componentDidLoad() {
console.log(this.element); // outputs HTMLElement <my-component ...
// loop over NodeList as per https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/
const list = this.element.querySelectorAll('li.my-list');
[].forEach.call(list, li => li.style.color = 'red');
}
render() {
return (
<div class="my-component">
<ul class="my-list">
{ this.data.map(count => <li>{count}</li>)}
</ul>
</div>
);
}
}

From the official docs
In cases where you need to get a direct reference to an element, like you would normally do with document.querySelector, you might want to use a ref in JSX.
So in your case:
import { Component, Prop } from '#stencil/core';
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Prop() first: string;
#Prop() last: string;
divElement!: HTMLElement; // define a variable for html element
getElementHere() {
this.divElement // this will refer to your <div> element
}
render() {
return (
<div ref={(el) => this.divElement= el as HTMLElement}> // add a ref here
Hello, World! I'm {this.first} {this.last}
</div>
);
}
}

you can get the current HTML element adding this into your component as property:
#Element() myElement: HTMLElement;
You can read more about this here
Hope this helps you :)

Related

Root for IntersectionObserver using getElementById always null

On an Angular project, I want to restrict the viewport of an IntersectionObserver to a specific part of a DOM.
I define the element to use as root with an id:
<div id="container">
<div class="page-list">
<infinite-scroll-page (scrolled)="onInfiniteScroll()">
...
</infinite-scroll-page>
</div>
</div>
In the corresponding component I define the root using getElementById:
export class InfiniteScrollPageComponent implements OnDestroy {
#Input() options: {root, threshold: number} = {root: document.getElementById('container'), threshold: 1};
#ViewChild(AnchorDirectivePage, {read: ElementRef, static: false}) anchor: ElementRef<HTMLElement>;
private observer: IntersectionObserver;
constructor(private host: ElementRef) { }
get element() {
return this.host.nativeElement;
}
ngAfterViewInit() {
const options = {
root: document.getElementById('container'),
...this.options
};
console.log("options: ", JSON.stringify(options));
//...
But the root loged is always null.
What do I do wrong ?
Firstly, your spread operator is the wrong way around, so unfortunately you're overwriting your root assignment immediately after it is set with the default value in the #Input() (which as far as I can see is not actually used as an input?).
Reversing this might be all you need to fix the issue:
const options = {
root: document.getElementById('container'),
...this.options
};
should be
const options = {
...this.options,
root: document.getElementById('container')
};
Secondly, I wonder whether it would make more sense to use #ViewChild and pass a reference to the container element into the InfiniteScrollPageComponent from the parent.
parent.component.html
<div
#Container
id="container">
<div class="page-list">
<infinite-scroll-page
*ngIf="containerRef"
[containerRef]="containerRef"
(scrolled)="onInfiniteScroll()">
...
</infinite-scroll-page>
</div>
</div>
parent.component.ts
export class ParentComponent {
#ViewChild('Container') containerRef: ElementRef<HTMLDivElement>;
}
infinite-page-component.component.ts
export class InfiniteScrollPageComponent {
#Input() containerRef: ElementRef<HTMLDivElement>;
ngAfterViewInit() {
const options = {
...this.options
root: containerRef.nativeElement,
};
}

How to get text content of slot?

We can have stencilJS element with slot as below
<my-component>123</my-component>
I'm trying to get the value of 123 from my render method itself, wondering if that is possible?
#Component({ tag: 'my-component' })
export class MyComponent {
render() {
return (
<div><slot /></div>
)
}
}
I would like to do some string formatting on 123 instead of rendering slot directly
import { Element } from '#stencil/core';
#Component({ tag: 'my-component' })
export class MyComponent {
/**
* Reference to host element
*/
#Element() host: HTMLElement;
componentWillRender() {
console.log(this.host.innerHTML)
}
render() {
return (
<div><slot /></div>
)
}
}
In web components, the content inside of it is part of the main DOM, too. This content is not going to show if you don't use slots; but, the content is going to project next to the #shadow-root anyway (check it using the chrome developer tools in the "elements" section).
So, if you do not want to show the content using default slots, you can use the property decorator #Element() and declare a property of type HTMLElement:
Then, you can access to the content via innerHTML or innerText.
Finally, you can format the content. Check the code snippet bellow:
import { Component, Element, h } from "#stencil/core";
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Element() element: HTMLElement;
formatContent(content: any) {
if ( isNaN(content)){
// Your format here
return content;
} else {
return content + '.00';
}
}
render() {
return [
// Commented slot tag
// <slot></slot>,
<div> {this.formatContent(this.element.innerHTML)} </div>
];
}
}
Using three times the web component with 2 strings and a number as a entry data, the result should be:
My text
My text 2
123.00

Angular 2 - Get passed object to component via inputs

On my parent page I have a link here:
<a (click)="showPermissionsRates(5757);">Link</a>
The function sets it:
showPermissionsRates(item) {
this.currentEventPoolId = item;
}
With a child component on the parent page here:
<app-event-pools-permissions-rates [eventPoolId]="currentEventPoolId "></app-event-pools-permissions-rates>
And then in my child component TS file I use:
inputs: ['eventPoolId']
But how do I get that value of '5757' in the child component? Such as using alert?
You should be able to just use #Input() on the child property.
I've put this together showing a VERY basic example, but without more to go on regarding your issues, it's hard to know what you need:
https://plnkr.co/edit/y9clOla1WrPFmhMJoz7o?p=preview
The gist is to use #Input() to mark your inputs in the child component, and map those in the template of the parent.
import {Component} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import { ChildComponent } from 'child.component.ts';
#Component({
selector: 'my-app',
template: `
<div>
<button (click)="changeProperty('ABC 123')">Click Me!</button>
<child-component [childProperty]="parentProperty"></child-component>
</div>
`,
})
export class App {
public parentProperty: string = "parentProp";
public changeProperty(newProperty: string) : void {
this.parentProperty = newProperty;
}
}
Then, in the child:
import {Component, Input} from '#angular/core'
#Component({
selector: 'child-component',
template: `
<div>Hello World: {{ childProperty }}</div>
`,
})
export class ChildComponent {
#Input()
childProperty:string;
constructor() {
this.childProperty = 'childProp'
}
}
I think you are setting value to at input variable in a click event, then you have to listen for it in the child component constructor using ngonchanges
ngOnChanges(changes: SimpleChanges) {
if(changes['eventpoolid'] && changes['eventpoolid'].currentValue) {
// you get updated value here
}
}

Angular2: Component class and underlying class

I often have the case that I want a component which renders to a DOM element and which needs some data for that. This data comes from objects I store in the parent component like that:
#Component({
template: '<child *ngFor="let child of children" [child]="child"/>'
})
export class ParentComponent {
children: Array<Child>;
constructor () {
this.children = [
new Child('Foo'),
new Child('Bar'),
]
}
}
class Child {
name: string;
constructor (name) {
this.name = name;
}
}
#Component({
selector: 'child',
template: 'My name is {{child.name}}'
})
export class ChildComponent {
#Input child: Child;
}
Here, Child and ChildComponent represent the same entity. But despite that I have to use two classes and two instances for each entity instance and I have to pass one object to the 'wrapping' component class. That seems unnecessarily complicated to me.
I would find it much easier if I could somehow 'merge' the classes Child and ChildComponent like that (imaginary syntax):
#Component({
template: '<child *ngFor="let this of children"/>'
})
export class ParentComponent {
children: Array<MergedChildComponent>;
constructor () {
this.children = [
new MergedChildComponent('Foo'),
new MergedChildComponent('Bar'),
]
}
}
#Component({
selector: 'child',
template: 'My name is {{name}}'
})
export class MergedChildComponent {
name: string;
constructor (name: string) {
this.name = name;
}
}
Is there a way to achieve that?

Angular 2 #Input - How to bind ID property of child component from parent component?

In my parent component, I want to create a child component with a unique ID associated with it, and I want to pass that unique ID into the child component, so the child component can put that ID on its template.
Parent template:
<ckeditor [ckEditorInstanceID]="someUniqueID"> </ckeditor>
Here is the child component:
import { Component, Input } from '#angular/core'
var loadScript = require('scriptjs');
declare var CKEDITOR;
#Component({
selector: 'ckeditor',
template: `<div [id]="ckEditorInstanceID">This will be my editor</div>`
})
export class CKEditor {
#Input() ckEditorInstanceID: string;
constructor() {
console.log(this.ckEditorInstanceID)
}
ngOnInit() {
}
ngAfterViewInit() {
loadScript('//cdn.ckeditor.com/4.5.11/standard/ckeditor.js', function() {
CKEDITOR.replace(this.ckEditorInstanceID);
console.info('CKEditor loaded async')
});
}
}
What am I missing? I can't seem to get the child component to receive the value of "someUniqueID". it is always undefined.
UPDATE: I was able to get the child component to receive the value "someUniqueID. Code udpated above. However, I cannot reference the #Input property by calling this.ckEditorInstanceID because this is undefined.
How do I reference the property I brought in via #Input?
Don't name inputs id. That's conflicting with the id attribute of the HTMLElement.
The trick was to use an arrow function like #David Bulte mentioned.
loadScript('//cdn.ckeditor.com/4.5.11/standard/ckeditor.js', () => {
CKEDITOR.replace(this.ckEditorInstanceID);
console.info('CKEditor loaded async')
});
For some reason the arrow function can access this.ckEditorInstanceID, but a regular function() {} cannot access this.ckEditorInstanceID. I don't know why, maybe someone can enlighten me to the reasoning for this.
In addition, I had to change my markup like this:
<ckeditor [ckEditorInstanceID]="'editor1'"> </ckeditor>
<ckeditor [ckEditorInstanceID]="'editor2'"> </ckeditor>
And set the #Input property to the name inside the [] which is ckEditorInstanceID , and also the template source should be the property name ckEditorInstanceID, like [id]="ckEditorInstanceID" .
Full working child component that receives the ID from the parent html selector:
import { Component, Input } from '#angular/core'
var loadScript = require('scriptjs');
declare var CKEDITOR;
#Component({
selector: 'ckeditor',
template: `<div [id]="ckEditorInstanceID">This will be my editor</div>`
})
export class CKEditor {
#Input() ckEditorInstanceID: string;
constructor() {}
ngAfterViewInit() {
loadScript('//cdn.ckeditor.com/4.5.11/standard/ckeditor.js', () => {
CKEDITOR.replace(this.ckEditorInstanceID);
console.info('CKEditor loaded async')
});
}
}

Categories