StencilJs/Jsx: render HTMLElements in nested component - javascript

This is my component:
#Component({
tag: "my-alert-list",
styleUrl: "alert-list.scss",
shadow: true,
})
export class AlertList {
#State() alertList: object[] = [];
#Method()
async appendAlert(
type: string,
message: string,
htmlContent: object,
canClose: boolean = false,
closeDelay: number
) {
let alertBoxElement = (
<my-alert-box
alert-type={type}
message={message}
can-close={canClose}
close-delay={closeDelay}
opened
>
{htmlContent}
</my-alert-box>
);
this.alertList = [
...this.alertList,
alertBoxElement
]
}
render() {
return (
<Host>
{this.alertList}
</Host>
);
}
}
The method appendAlert aims to append a new my-alert-box element to the list of alerts.
In same case i don't want to pass a simple text to the my-alert-box but some HTML block.
(my-alert-box has a receiver slot element and i verified that it works).
I tried to achieve this with the htmlContent variable as you can see, but of course it doesn't work if i do:
$('#alertlist')[0].appendAlert(type='info',message='', htmlContent=document.createElement('div'))
I receive the error:
[STENCIL-DEV-MODE] vNode passed as children has unexpected type.
Make sure it's using the correct h() function.
Empty objects can also be the cause, look for JSX comments that became objects.
Any idea on how can i achieve this?

It's not possible like this because JSX works differently. You could pass the htmlContent as a string and use innerHTML on my-alert-box but it's dangerous (XSS).
Ionic's ion-alert has the same limitation with the message prop... see https://ionicframework.com/docs/api/alert#properties which has a link to https://ionicframework.com/docs/techniques/security, and there they explain how they do some basic DOM sanitization (#ionic/core is also built with Stencil).

Related

Get text content from React element stored in a variable

Is there a way to get text content from a React element stored in a variable without ref?
There is a functional component, that receives title prop, which contains react element:
function component({ title }) {
const content = title.textContent() // Need Something like this
}
and this title prop might have react node like: <div>Some Title</div>. But I'd like to get only content of the node, in a variable before rendering it. Is it possible?
When I console.log title variable this is the output, The content I want is inside props.children array, so is there a method to get it without traversing through keys:
I've not found a better solution than indeed traversing the object to get the text. In TypeScript:
/**
* Traverse any props.children to get their combined text content.
*
* This does not add whitespace for readability: `<p>Hello <em>world</em>!</p>`
* yields `Hello world!` as expected, but `<p>Hello</p><p>world</p>` returns
* `Helloworld`, just like https://mdn.io/Node/textContent does.
*
* NOTE: This may be very dependent on the internals of React.
*/
function textContent(elem: React.ReactElement | string): string {
if (!elem) {
return '';
}
if (typeof elem === 'string') {
return elem;
}
// Debugging for basic content shows that props.children, if any, is either a
// ReactElement, or a string, or an Array with any combination. Like for
// `<p>Hello <em>world</em>!</p>`:
//
// $$typeof: Symbol(react.element)
// type: "p"
// props:
// children:
// - "Hello "
// - $$typeof: Symbol(react.element)
// type: "em"
// props:
// children: "world"
// - "!"
const children = elem.props && elem.props.children;
if (children instanceof Array) {
return children.map(textContent).join('');
}
return textContent(children);
}
I don't like it at all, and hope there's a better solution.
use https://github.com/fernandopasik/react-children-utilities
import Children from 'react-children-utilities'
const MyComponent = ({ children }) => Children.onlyText(children)
from https://github.com/facebook/react/issues/9255
Thanks #Arjan for the effort and solution, but I have changed something in the component, to get the title in string format.
Now I have added another props to the component: renderTitle which is a function to render custom react title.
So now I am passing title as string:
<Component
title="Some content"
renderTitle={(title) => <div>{title}</div> }
/>
and inside component:
<div>{renderTitle ? renderTitle(title) : title}</div>
With this implementation, I can use title as string to do what I want inside the component, while also supporting custom title render.

ReactJS: Join map output with concatenating value

In my ReactJS application I am getting the mobile numbers as a string which I need to break and generate a link for them to be clickable on the mobile devices. But, instead I am getting [object Object], [object Object] as an output, whereas it should be xxxxx, xxxxx, ....
Also, I need to move this mobileNumbers function to a separate location where it can be accessed via multiple components.
For example: Currently this code is located in the Footer component and this code is also need on the Contact Us component.
...
function isEmpty(value) {
return ((value === undefined) || (value === null))
? ''
: value;
};
function mobileNumbers(value) {
const returning = [];
if(isEmpty(value))
{
var data = value.split(',');
data.map((number, index) => {
var trimed = number.trim();
returning.push(<NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>);
});
return returning.join(', ');
}
return '';
};
...
What am I doing wrong here?
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
First question:
What am I doing wrong here?
The issue what you have is happening because of Array.prototype.join(). If creates a string at the end of the day. From the documentation:
The join() method creates and returns a new string by concatenating all of the elements in an array (or an array-like object), separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
Think about the following:
const navLinks = [{link:'randomlink'}, {link:'randomlink2'}];
console.log(navLinks.join(','))
If you would like to use concatenate with , then you can do similarly like this:
function mobileNumbers(value) {
if(isEmpty(value)) {
const data = value.split(',');
return data.map((number, index) => {
const trimed = number.trim();
return <NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>;
}).reduce((prev, curr) => [prev, ', ', curr]);
}
return [];
};
Then you need to use map() in JSX to make it work.
Second question:
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
Usually what I do for constants is that I create in the src folder a file called Consts.js and put there as the following:
export default {
AppLogo: 'assets/logo_large.jpg',
AppTitle: 'Some app name',
RunFunction: function() { console.log(`I'm running`) }
}
Then simply import in a component when something is needed like:
import Consts from './Consts';
And using in render for example:
return <>
<h1>{Consts.AppTitle}</h1>
</>
Similarly you can call functions as well.
+1 suggestion:
Array.prototype.map() returns an array so you don't need to create one as you did earlier. From the documentation:
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
I hope this helps!

In React, why does adding state that is a number to prop that is a number results in string?

If I do the following:
this.setState( { skip: this.state.skip + this.props.PageSize } );
the result is a string, (e.g. "0555" when it tries to add 5 every click)
During debugging, it shows this.state.skip as a number, but this.props.PageSize as a string. However, the prop is defined as a number:
export interface IMyProps{
Description: string;
Context: WebPartContext;
Environment: string;
PageSize: number;
}
Do I always have to parse React props? Seems odd.
Edit: trying to do the following:
parseInt(this.props.Pagesize)
fails because, well obviously, you can't parse a number to an int, only a string.
Based on the details you provided, it looks like a logic issue but you can change your code like below to fix this issue
const { PageSize } = this.props;
this.setState(prevState => ({ skip: prevState.skip + parseInt(`${PageSize}`) }));
prevState.skip
We should use function inside setState when new state should be updated based on the current state.
parseInt(${PageSize})
This make sure that you are having string value and then parse it to Integer.
This won't be required if you make sure that number is passed instead of string
As commented by patrick:
Anything you pass inside the quote will be string.
myprop="1" // 1 is string here
myprop={1} // 1 is number here
So, you have to use curly brace to use number or variable but not quote.
Hint: You may also use the following to fix your issue:
this.setState( { skip: this.state.skip + +this.props.PageSize } );

Unable to render an array of values (React component)

I am building a test app to learn more about React and I have made an API call which gets a huge JSON object.
I was able to break this json into the parts that I need and now I have 10 arrays of 3 props each. I am able to send these 10 arrays in 3 props to another component, which needs to use these 3 props 10 times and render a div class Card each.
I can console.log(this.props) and it shows 10 different arrays with 3 props each,however, I cannot produce a same element 10 times.. I tried using map() but since my array is initially undefined, map() is not able to function properly either. Is there any thing in react like *ngFor in Angular ?
What is the best way to go about this?
*EDIT
Here's more code guys. Sorry still noobie here..
ERROR : this.props.map is not a function
return(
<div>
{this.props.map((data,i)=>{
return(
<li key={i}>{data.likes}</li>
);
*EDIT 2
Soo I tried running map function with an if condition but the code still breaks the very moment the condition gets true..
render() {
if(this.props.url !== undefined){
this.props.map((data,i) =>{
return <li key={i}>{data.likes}</li>
})
}
My state method is :
state = {
userId: undefined,
likes: undefined,
url: undefined
}
and im setting my values on each data stream as follows :
const pics = await fetch(`${base_url}?key=${api_key}&q=${query}
&img_type=photo&per_page=12`).then(response => {
return response.json();
})
pics.hits.map((data) =>{
return this.setState({
userId: data.user_id,
likes: data.likes,
url: data.webformatURL
})
})
this.props won't have map, it's not an array. It's an object with a property for each property passed to your component. For instance:
<YourComponent foo="bar"/>
...will have this.props.foo with the value "bar".
So if you're passing an array to your component, like this:
<YourComponent theArrayProperty={[{likes: 42},{likes:27}]} />
...then you need the name of that property:
return (
<div>
{this.props.theArrayProperty.map((data,i) => {
return (
<li key={i}>{data.likes}</li>
);
})}
</div>
);
Side note: You can use a concise arrow function for the map callback instead:
return (
<div>
{this.props.theArrayProperty.map((data,i) => <li key={i}>{data.likes}</li>)}
</div>
);
...and no need for the () if you put the opening tag on the line with return (you can't leave off the ( if it's on the next line, but you probably knew that):
return <div>
{this.props.theArrayProperty.map((data,i) => <li key={i}>{data.likes}</li>)}
</div>;
...but that's a matter of style.
With little information that you have provided, my guess is that code fails at map() when you try to use it with undefined value.
Try adding a conditional check to render
{props && props.map([RENDER CODE HERE])}
You can just make simple if statement to check if the array is not undefined, and then pass it to map function.
Another option is to set a defaultProps for an empty array.
MyComponent.defaultProps = {
arrProp: []
};

Reactjs how to convert string to [object Object]

I have a menuItem component and its props is a Icon component created by me and called FontIcon.
And in menuItem props you can pass a name of the icon as a string, for example: leftIcon="face" but you can also pass a code for the component like this: leftIcon='<FontIcon style={{color:"red"}} className="face" />'.
In the first case, all works perfectly, the props is passed to the variable where is code for the component:
leftIcon = <FontIcon size={_props.fontSize} className={_props.leftIcon} />;
But this second way is not working. When the user passes all code I need to add something to this(this size value like above), at this point this adding works:
leftIcon = _props.leftIcon.replace(/\/>$/, " size={_props.fontSize}/>");
Here I've got if to check what has the user passed:
if (_props.leftIcon.match(/<.+\/>/)) {
leftIcon = _props.leftIcon.replace(/\/>$/, " size={_props.fontSize}/>");
} else {
leftIcon = <FontIcon size={_props.fontSize} className={_props.leftIcon} />;
}
But in this second way, I'm getting a string and it doesn't work. There is no icon, but there is a string with code:
So I've consoled log this and it's what I got:
The typeof this first is object but this second one is string.
So what can do to make this second way works?
If this code is in the render portion this should work;
if (_props.leftIcon.match(/<.+\/>/)) {
leftIcon = _props.leftIcon.replace(/\/>$/, " size={_props.fontSize}/>");
} else {
leftIcon = (<FontIcon size={_props.fontSize} className={_props.leftIcon} />);
}
Note the parenthesis.
There's a way to create component from string you can take a look on this answer
But I would suggest you to pass styles and / or class names and other parameters to your component rather then a string and then you could just have:
return <FontIcon class={condition ? 'firstClass' : 'secondClass'} ... />;

Categories