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
Related
I'm using Storybook 6.5.9 to render web components (made with Stencil). I have several of them working correctly but now I'm creating a story for a new component that can receive a property that is an array of objects.
The component would be something like this:
import {Component, Host, h, Prop} from '#stencil/core';
#Component({
tag: 'test-component',
})
export class TestComponent {
/**
* Array of options for the group.
*/
#Prop() items?: String[] = [];
render() {
return (
<Host>
{
this.items.map(item => (
<h1>{item}</h1>
))
}
</Host>
)
}
}
And this is the story:
import { Story, Meta } from '#storybook/html';
export default {
title: 'Components/TestComponent',
parameters: {
options: {
showPanel: true
},
}
} as Meta;
const Template: Story = (args) => {
return `
<test-component items="${['some', 'thing']}">
</test-component>
`;
};
export const TestComponent: Story = Template.bind({});
I have tried setting the items property to a string but the component never gets anything, it's always an empty array.
I'm not getting any errors either in the console. I'm definitely doing something wrong but I don't know what it is... I've been using several other data types for those properties (boolean, string, numbers...) but this is the first time I'm using objects/arrays and I'm not able to get it to work.
Any help will be highly appreciate it.
Thanks!
Properties are passed in as strings in HTML.
Use JSON.parse(this.items) in your render() method in case this.items is not an array:
import { Component, Host, h, Prop } from '#stencil/core';
#Component({
tag: 'my-component',
})
export class MyComponent {
/**
* Array of options for the group.
*/
#Prop() items?: string | string[] = '';
render() {
return (
<Host>
{(Array.isArray(this.items) ? this.items : JSON.parse(this.items)).map(item => (
<h1>{item}</h1>
))}
</Host>
);
}
}
For that to work, you need to pass in your items as valid JSON, meaning you have to use single attribute quotes and double quotes for the "strings" in the "array":
const Template: Story = (args) => `
<test-component items='${["some", "thing"]}'>
</test-component>
`;
If you are receiving [object Object] in your component, try to stringify your prop value before passing:
const Template: Story = (args) => `
<test-component items='${JSON.stringify(["some", "thing"])}'>
</test-component>
`;
You can not set arrays or objects as properties via HTML (only in JSX/TSX). You have two options:
pass them as a JSON string and parse them back into an array/object; you’d basically have to type them as a string in your component, and then cast the type after parsing them
set the property via a script; there’s two options, depending on whether or not the option is required or not.
<test-component></test-component>
<script>
document.querySelector("test-component").items = ["foo"]
</script>
or if the prop is required:
<script>
const testComponent = document.createElement("test-component")
testComponent.items = ["foo"]
document.body.appendChild(testComponent)
</script>
I'd like to be able to access the SearchResults component, (when it has been clicked), in the root component (AppComponent) as I'm looking to set different properties on the SearchResults component such as;
I'd like to set an attribute on the SearchResults component so that it shows the "close" text
Also, I'd to set the click event on the SearchResults to redirect elsewhere or actually enable it as a multi-select so that it stays selected until a user proceeds to the next step for example.
I'm trying to make the SearchResults and SearchResult components as re-usable as possible so we're able to state in the parent component which would include the <app-searchresults> selector what action we'd like our SearchResults components to actually be when they are clicked.
The only way I can really see doing this is using EventEmitter to pass the event up once through the SearchResult component then onto the parent component and then a Service to hold selected values but I'm still stuck around enabling the SearchResults component as either a component which redirects when clicked or stays selected? Is this actually possible or do I need to create a different SearchResults component for each different state I'd like?!
export class AppComponent {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
name = 'Angular';
ngAfterViewInit() {
this.components.changes.subscribe((r) => { console.log(r) });
}
}
SearchResults.ts
#Component({
selector: 'app-searchresults',
templateUrl: './searchresults.component.html',
styleUrls: ['./searchresults.component.css']
})
export class SearchresultsComponent implements OnInit {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
constructor() { }
ngOnInit() {
}
}
SearchResults.html
<h1>Search Results<h1>
<app-searchresult result ="first"></app-searchresult>
<app-searchresult result ="second"></app-searchresult>
<app-searchresult result ="third"></app-searchresult>
SearchResult.ts
#Component({
selector: 'app-searchresult',
templateUrl: './searchresult.component.html',
styleUrls: ['./searchresult.component.css']
})
export class SearchresultComponent implements OnInit {
#Input()
result: string;
isSelected: boolean;
constructor() { }
ngOnInit() {
}
toggleClickedState(){
if(!this.isSelected){
this.isSelected = !this.isSelected;
}
}
}
SearchResult.html
<div>
<p (click)=toggleClickedState() [ngClass]="isSelected? 'selected' : '' "> Search Result : {{result}}</p>
<p *ngIf="isSelected" class="cross" (click)="isSelected = false;">close</p>
<div>
I've included a link to structure of an app that references the above;
https://stackblitz.com/edit/angular-cjhovx
I have a VueJS component,
comp.vue:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
data () {
return {
}
},
}
</script>
And I call this Vue component just like any other component:
...
<comp>as a title</comp>
<comp>as a paragraph</comp>
...
I would like to change comp.vue's slot before it is rendered so that if the slot contains the word "title" then the slot will be enclosed into an <h1>, resulting in
<h1>as a title</h1>
And if the slot contains "paragraph" then the slot will be enclosed in <p>, resulting in
<p>as a paragraph</p>
How do I change the component slot content before it is rendered?
This is easier to achieve if you use a string prop instead of a slot, but then using the component in a template can become messy if the content is long.
If you write the render function by hand then you have more control over how the component should be rendered:
export default {
render(h) {
const slot = this.$slots.default[0]
return /title/i.test(slot.text)
? h('h1', [slot])
: /paragraph/i.test(slot.text)
? h('p', [slot])
: slot
}
}
The above render function only works provided that the default slot has only one text child (I don't know what your requirements are outside of what was presented in the question).
You can use $slots(https://v2.vuejs.org/v2/api/#vm-slots):
export default {
methods: {
changeSlotStructure() {
let slot = this.$slots.default;
slot.map((x, i) => {
if(x.text.includes('title')) {
this.$slots.default[i].tag = "h1"
} else if(x.text.includes('paragraph')) {
this.$slots.default[i].tag = "p"
}
})
}
},
created() {
this.changeSlotStructure()
}
}
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 :)
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')
});
}
}