Get text content from React element stored in a variable - javascript

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.

Related

Is there a way to find out if a JS function return JSX or not?

I have a dynamic list. Developers can either provide a URL or a component for that list. Both of them are basically functions:
<List
{...otherProps}
create={params => `create_a_dynamic_url`}
/>
<List
{...otherProps}
create={Form}
/>
and in my List.jsx I have a create prop that is a function.
I should either directly render it using {create} or call it first and then navigate the user to that URL.
For both of them, the typeof operator returns function. Is there a way to find out if their return value is JSX or not?
It is not reliably possible to check if the function is a React component,
but it might help you to check for the React element (which is easy).
Check the React element
You can call the function yourself, assign the return value to a variable, and check if that is a React element.
function App(){
return <>
<List id={'A'} create={ props => <>(My React component)</> } />
<List id={'B'} create={ params => `create_a_dynamic_url` } />
</>;
}
export const List = props => {
const { create, id } = props;
// -- Check for the React components `.name`:
console.log('(List.js) create.name:', id, create?.name);
// --
// Create either a ReactElement from the ReactComponent,
// or the string from the string constructor function:
// --
const myElementOrString = create();
// -- Check for string or ReactElement:
const isString = typeof myElementOrString === 'string';
const isReactElement = !isString;
// --
return <>
<p>{ id }</p>
<div>
<b>The React element is:</b>
{ isReactElement ? myElementOrString : '(none)' }
</div>
<p>
<b>The string is:</b>
{ isString ? '"' + myElementOrString + '"': '(none)' }
</p>
</>;
};
Alternatively you could also check for myElementOrString.hasOwnProperty('$$typeof') or other React specific properties,
but I would be careful with that, because these (or some of them) are "implementation details" you should not rely on.
The .name property
Also note that the function received by your List component via the create property
will have a .name property, which will be different depending on some circumstances, e.g.:
an anonymous function will have the name "create" (the property name)
a named component will have the name of the component
a named function will have the name of the function
... ?
You might be able to check the .name property, but I think that approach would not
be reliable (e.g. this depends on that the developer knows about this, e.g. has to name the function in a specific way.)

Is there a way to manipulate rendered text in react component children?

I am trying to write a component that highlights text inside it's children recursively.
What I have been able to achieve, is to highlight the text only if it's explicitly provided in the body component, but I can't find a way to change the text of the component's render part.
Let's say I have the following HighlightText component:
(Note, that this is a concept component. The real component is much more complicated)
const HighlightText = ({highlight, children}) => {
const regex = new RegExp(`(${regexEscape(highlight)})`, 'gi');
return React.Children.map(children, child => {
// Found a text, can highlight
if (typeof child === 'string') {
const text = child.trim();
if (text) {
return text.split(regex).filter(p => p).map((p, i) =>
regex.test(p) ? <mark key={i}>{p}</mark> : <span>{p}</span>;
);
}
}
// If child is a react component, recurse through its children to find more text to highlight
if (React.isValidElement(child)) {
if (child.props && child.props.children) {
return HighlightText({children: child.props.children, highlight});
}
}
// Here I believe, should be another handling that handles the result of the render function to search for more text to highlight...
// For any other cases, leave the child as is.
return child;
})
}
And some component that renders something:
const SomeContent = () => <div>content</div>;
Now, I want to use the HighlightText component the following way:
ReactDOM.render(
<HighlightText highlight="e">
<SomeContent />
<p>some paragraph</p>
nude text
</HighlightText>
,document.body);
The resulted DOM of the the above code is:
<div>content</div>
<p><span>som</span><mark>e</mark><span> paragraph</span></p>
<span>nud</span><mark>e</mark><span> t</span><mark>e</mark><span>xt</span>
But I expect it to be:
<div><span>cont</span><mark>e</mark><span>nt</span></div>
<p><span>som</span><mark>e</mark><span> paragraph</span></p>
<span>nud</span><mark>e</mark><span> t</span><mark>e</mark><span>xt</span>
Any suggestions on how to handle the rendered part of the child component?
Eventually I managed to solve this problem using React.Context.
Not exactly as I expected, but I think it's even a better approach, because now I can decide what text to highlight.
It's similar to i18n and themes techniques in React. React.Context is best approach for these kind of text manipulations.

StencilJs/Jsx: render HTMLElements in nested component

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).

Draft.js - CompositeDecorator: Is there a way to pass information from the strategy to the component?

Lets say my strategy calculates some numbered label. How can I pass this (e.g. via props) to the decorator component.
I know there is a props property in CompositeDecorator but how can I access it from the strategy function?
I'm a bit new to DraftJs, but based on my understanding:
Strategies should be used to identify the range of text that need to be decorated. The rendering of that decoration (which presumably includes calculating what the label should be) should be handled in the component itself, rather than the strategy.
You should be able to access the ContentState via the props object in your component, and calculate the label from that. The constructor of your component could be a good place for executing the logic for calculating the label. This also means that you might have to use a class definition for your decorator components as opposed to a pure function as shown in the examples on the draftjs website.
You can also circumvent the issue by reading the values from the text with regex. The following example is done with #draft-js-plugins:
// How the section is detected.
const strategy = (contentBlock, callback) => {
const text = contentBlock.getText();
const start = text.indexOf('<_mytag ');
const endTag = '/>';
const end = text.indexOf(endTag);
if (start !== -1 && end !== -1) {
callback(start, end + endTag.length);
}
};
// What is rendered for the detected section.
const component = ({ decoratedText }) => {
if (decoratedText) {
const label = decoratedText.match(/label="([a-zA-Z0-9/\s]*?)"/);
if (
label &&
typeof label[1] === 'string'
) {
return <div>{label[1]}</div>;
}
return null;
}
};
export const MyTagPlugin = {
decorators: [
{
strategy,
component,
},
],
};

How to replace the value of an array of objects in React

I have this function that returns an array of objects, every object represent a sticky, what I want is to change the value of "content" everytime I click one of the stickies
handleStickyEdition = (index) => {
const { currentStage } = this.props
const stickies = currentStage.get('stickies')
const updatedStickies = [...stickies]
console.log(updatedStickies)
}
And the result of calling the console.log is this array of objects:
If I do console.log(updatedStickies[index].get('content')) I will get the content of the object I want to change. For example name 3.
How can I replace this content with an empty string? in other words, if I click the object in the position 0, how can I make name 3 equals to ''
I would suggest using a map like so.
this.setState({
updatedStickies: this.state.updatedStickes.map(sticky => ({
...sticky
content: sticky.id === idOfStickyWeWantToUpdate ? "" : "content"
}))
});
I see you are reading stickies from props, I would suggest having a function in your parent component to run the above code which you can call from your child component if need be.

Categories