React render html from a string with a dynamic variable - javascript

I am working with react app in typescript. From API, I have this input:
a) list of variable names ["name", "surname"]
b) few strings in form of simple html with variables "<p>Hello, how are you {name}?</p>"
c) number of inputs with variables such as {input1: "name"}
everything as a string/JSON
what i need to do is: render simple html (only few tags) received from API but "create" binding between those dynamic inputs and variables in strings
in static world, result would look like:
[name, setName] = useState("")
<p>Hello, how are you {name}?</p>
<input type="text" onChange={e => setName(e.target.value)}/>
However, all of this is dynamic. String "<p>Hello, how are you {name}?</p>" doesnt get binded to the input on its own.
I tried:
setting variable [vars, setVars] = useState({}), property for each dynamic variable, with
a) dangerouslySetInnerHTML - renders only html (cannot bind the variable inside to the input)
b) react-html-parser - same as above
c) babel.transform - couldnt make it work as this is done dynamically and in browser, it cannot find the right preset, i couldnt make the mimified babel.js work with typescript How to render a string with JSX in React
do you see some easy way? For example how could i use React.createElement to render html with "live" variable inside, represented as {variableName}? Or maybe something out of the box? giving each elemnt a class and finding the class in DOM and editing the text with input change would be probably very non-optimal?
I hope this could be a better example:
{response:
{
variables: ["name", "name2", "mood"],
texts: [
"<em> Hello! My name is {name}</em>",
"<p> Hi {name} ! I am <strong>{name2}</strong> and I feel {mood} today</p>"
],
inputs: [
{
label: "How do i feel?"
input: {mood}
}
]
}
}

EDIT:
This should give you a good idea:
https://stackblitz.com/edit/react-ts-cwm9ay?file=DynamicComponent.tsx
EDIT #2:
I'm pretty good with React and interpolation, it's still in progress (specifically the docs, but the readme is complete) but I'm going to shamelessly plug my ReactAST library
EDIT #3 - If you're interested in doing crazy dynamic interpolation, then you might also want to check out a neat dynamic interpolation (and it's reverse) library
Let's assume this:
{
vars: ['name','surname'],
strings: ["<p>Hello, how are you {name}?</p>","<p> It's night to meet you {name} {surname}"],
inputs: {
input1: 'name',
input2: 'surname'
}
}
Create a component (or set of components) that can do this.
I haven't even ran any of this, but here's the idea. I'll put it in a stackblitz in a bit and polish it up to make sure it works:
const MyDynamicComponent = ({vars,strings,inputs}) => {
const [state,setState] = useState(vars.reduce((acc,var) => ({...acc,[var]:undefined}));
return (
<>
<MyDynamicText vars={vars} strings={strings} state={state}/>
<MyDynamicInputs onChange={(name,val) => setState({[name]:val}) state={state} inputs={inputs}/>
</>
)
}
const MyDynamicText = ({vars,strings,state}) => {
return strings.map((s,i) => s.replaceAll(`{${vars[i]}}`,state[vars[i]])
}
const MyDynamicInputs = ({onChange,inputs,state}) => {
return Object.entries(inputs).map(([inputName,varName]) => <input key={inputName} onChange={e => onChange(varName,e.target.value)} value={state[varName]}/>
}

Related

React - filtering items from an array in any order

I'm sorry that this question has been asked in a similar fashion. I've read through a lot of similar threads but just don't know enough to apply it to my little project. I'm very new to React so I would appreciate any help!
Getting to it, I'm making a pokemon game where someone can hit click buttons to filter down a JSON list of pokemon. They can go by type,weaknessess,etc.
But I'm a little confused in keeping a 'global array' if that's the right word. It may be something I don't understand with React's useState.
Here's my code
import React, { useState, useEffect } from "react";
import "./App.css";
import PokemonLibrary from "./data/PokemonList.json";
export default function App() {
const [pokemonData, setPokemonData] = React.useState(PokemonLibrary.pokemon);
const pokeListCopy = PokemonLibrary.pokemon;
const filterTypeOne = () => { //filter type
const myFilteredPoke = pokeListCopy.filter((pokeType) => {
return pokeType.type.includes("Grass");
});
console.log(myFilteredPoke); // shows array of objects of left over pokemon
setPokemonData(myFilteredPoke);
};
const filterWeakness = () => { //filter weakness
const myFilteredPoke = pokeListCopy.filter((pokeType) => {
return pokeType.weaknesses.includes("Ice");
});
setPokemonData(myFilteredPoke);
};
return (
<div className="App">
<h1>Pokemon Selector!</h1>
<div>
<button onClick={filterTypeOne}>Filter Grass</button>
<button onClick={filterWeakness}>Weak to Ice</button>
</div>
{pokemonData &&
pokemonData.map((poke) => (
<p key={poke.id}>
#{poke.num} | {poke.name} | {poke.type[0]} {poke.type[1]}
<img src={poke.img} alt="Pokemon Images"></img>
</p>
))}
</div>
);
}
My question is, how do I keep a consistent array for these two functions (and more) to pull the same data from? I'd like it to be able to be filtered in either order. But currently, these filter separate arrays. I've played with it a lot using normal JavaScript, but I can't quite figure this out.
I hope that was enough information. Please let me know if this didn't make sense! I'd appreciate any guidance. Thank you.
Problem
You are facing this problem because you try to set the state of the list in an "imperative" manner, but React is meant to be used more "declarative". That means, what you try here is like:
"if I click the button, change the list to contain the items that contain 'Grass'",
but how you should use React is:
"if I click the button, then the state of my component should be that the list only contains items with 'grass'"
That might sound like the same, but there is a subtle difference. React is good in changing a state dependent on some other state. You might say "but that's what I'm trying to do, changing the state of the list", but then you have tell the full story
"if I click the button, and the list is not filtered already, and maybe contains the items X, then change the list to contain the items that contain 'Grass', unless ..."
This becomes quite complicated, especially comparing contents of lists and components.
Solution
There are different solutions to your problem, but what you should do is basically something like:
set the component state to describe what you want
have other parts of your program (and React) take care to give you a the list dependent on this description
e.g.:
const [pokemonData, setPokemonData] = React.useState(PokemonLibrary.pokemon);
const [listState, setListState] = React.useState({ type: '', weekness: '' });
useEffect(() => {
let newList = pokemonData;
if( listState.type === 'Grass' ){
newList = newList.filter( ... );
}
if( listState.weekness === 'Ice' ){
newList = newList.filter( ... );
}
setPokemonData(newList);
}, listState );
return (
<div>
<button onClick={()=>{ setListState({ ...listState, type: 'Grass' }) }}>Filter Grass</button>
{ pokemonData.map( (poke) => ... ) }
</div>
);
(This code is not very elegant and would not even work, and should only illustrate the idea. From here on there are several ways how to implement the filtering mechanism)

"JavaScript: Uncaught Invariant Violation: Objects are not valid as a React child" - on second of two identical date inputs

I'm getting the above error on the second, of what look like identical (to me) pieces of code! They both use the standard react-datepicker package.
First:
<div>
<label>Date Terminated</label>
<div>
<DateControl
value={this.state.edit_course.date_terminated || null}
name="date_terminated_course"
onChange={(e) => this.handleDetailChangeCourse("date_terminated", e.target.value)}
/>
</div>
</div>
Second:
<div>
<label>Date Terminated</label>
<div>
<DateControl
value={this.state.edit_prereq.date_terminated_prereq || null}
name="date_terminated_prereq"
onChange={(e) => this.handleDetailChangePrereqs("date_terminated", e.target.value)}
/>
</div>
</div>
As soon as the handleDetailChangePrereqs() call is made the error occurs during the render.
The two handle routines look like this:
handleDetailChangeCourse(prop, value) {
if (prop === "date_terminated") {
console.log(`date: ${value}`)
console.log(typeof value)
console.log(JSON.stringify(value))
}
const index = this.state.index
const courses = this.state.temp_courses
courses[index][prop] = value
this.setState({ temp_courses: courses });
}
handleDetailChangePrereqs(prop, value) {
if (prop === "date_terminated") {
console.log(`date: ${value}`)
console.log(typeof value)
console.log(JSON.stringify(value))
}
const index = this.state.prereqIndex
const prereqs = this.state.temp_prereqs
prereqs[index][prop] = value
this.setState(() => ({ temp_prereqs: prereqs }));
}
When I run the code and select/enter a date for the first control handleDetailChangeCourse)() it works fine and outputs the following:
date: 1608069600000
object
"2020-12-15T22:00:00.000Z"
But when I do the same for the second control handleDetailChangePrereqs() it errors after giving me this output:
date1: 1608069600000
object
"2020-12-15T22:00:00.000Z"
I understand the error message, and having read lots of solutions on this site most of them usually final they are rendering an object and not what they thought was a string.
I seem to be rendering an object in both cases but the first one works.
Having spent many hours logging out all sort of information I am lost as to what else I can look at. There are other DateControl calls within this same program and they work fine too. It is the second one that also renders an object's .date_terminated property that fails (albeit a separate object in each case).
The DateControl component looks like this:
setDate(selected_date) {
this.props.onChange({
target: {
name: this.props.name,
value: selected_date
}
});
},
render() {
return (
<DatePicker
disabled={this.props.readOnly}
onBlur={this.props.onBlur}
style={this.props.style}
dateFormat="DD/MM/YYYY"
locale="en-za"
placeholderText={this.props.readOnly ? "" : "DD/MM/YYYY"}
selected={this.props.value ? moment(this.props.value): null}
onChange={(date) => { this.setDate(date) } }
/>
);
}
Thanks to Shyam and Vishal for suggesting that I created a sandbox.
I spent quite a bit of time preparing some code to do just that before seeing where my problem was!
As I stated in my question: "I understand the error message, and having read lots of solutions on this site most of them usually final they are rendering an object and not what they thought was a string."
And I was doing just that.
The problem was not where I had decided it was before asking this question, it was elsewhere:
<table>
...
<td>
{ item.date_terminated_prereq }
</td>
...
</table>
As can be seen from the above code segment I was actually displaying the changed date as part of a table. The moment it rendered I got the error message.
So the learning that has come from this for me, is that everywhere on StackOverflow I have seen mention of this issue it has been because an 'object' was being displayed accidentally, or unknowingly.
I should have taken that in good faith and looked for it as a problem in my code, rather than stubbornly think the error was with the <DateControl />.
Note to reviewer:
Should I just delete this whole question as my answer is more philosophical than practical?

ReactIntl.FormattedPlural Component to String

I am new to React.
I used ReactIntl.FormattedPlural to format the plural.
<FormattedPlural
value={10}
one='message'
other='messages'
/>
It works when I place this component in the render() function. However, I have a scenario that I want to get the String and passed into a function.
Instead of Using {value ==1 ? 'message' : 'messages'}, can I use ReactIntl.FormattedPlural to achieve this?
In any case, in the end you need to put it into the render.
Because the FormattedPlural is a component. To display it, you need to render it. It's how React works.
You can save it as variable:
const text = (<FormattedPlural value={10} one='message' other='messages' />)
For example if you have a condition and need to decide which string to render.
But in the end, the text should passed to the render to be displayed for user.
Derived from FormattedPlural implementation:
const { formatPlural } = useIntl()
// get the plural category ("one" or "other")
const pluralCategory = formatPlural(count);
const options = {
one: "message",
other: "messages",
}
const plural = options[pluralCategory as 'one'] || options.other;
See also: https://formatjs.io/docs/react-intl/api/#formatplural

Save values of JS object as Markup

Is it possible to save markup on a JS object to be retrieved later?
Why?
Here lies my problem, if a description is too long, I'd like to be able to break it into separate chunks, perhaps different HTML tags, as opposed to having the entire text in one long chain of words
ie: after looping through object...
<div>{markup.description}</div>
the above would give me all the description data, but I wouldn't able to massage it (break into bold, italic, headings, or spans.) for a better UI.
So the end result that I'm trying to learn here is how to produce something like:
const markup = [
{
name: "<h1>Joe Doe<h1/>",
food: "<p>pizza<p/>",
description: "<h1>super long description<h2><p>bla bla
bla</p>"
}
]
I tried template literals but no dice.
I know I could separate chunks of text by adding more keys in the object, but that feels redundant because it is all a description, besides I still wouldn't be able to apply any styles (add a class) for words that need attention in the middle of the text.
I guess you can always make the properties functions.
const markup = [{
name: () => "<h1>Joe Doe<h1/>",
food: () => "<p>pizza<p/>",
description: () => "<h1>super long description<h2><p>bla bla bla</p>"
}]
function renderMarkup(item) {
let markup = ''
Object.entries(item).forEach(([key, value]) => {
markup += value()
});
return markup
}
$('.markup').html(renderMarkup(markup[0]))
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="markup"></div>
If you're using React and have JSX available, you can store JSX fragments in a variable and then reference them when you render. However, the markup you've written is very malformed. You have closing tags with the slash in the wrong place and you have an h1 matched up with an h2. JSX markup has to be valid, and each fragment has to be enclosed in a tag that contains the whole fragment.
This works:
const markup = [
{
name: <h1>Joe Doe</h1>,
food: <p>pizza</p>,
description: <div><h1>super long description</h1><p>bla bla bla</p></div>
},
{
name: <h1>Janet Doe</h1>,
food: <p>chicken</p>,
description: <div><h1>yet another super long description</h1><p>bla bla bla</p></div>
}
];
const App = () => (
<div>
{ markup.map(r => [r.name, r.food, r.description]) }
</div>
);

JSX can't parse partial HTML tag

I'm using react-rails and I need to be able to pass just a start HTML tag (ie: <form>) and just an end tag into an array (ie: </form>) in my JSX file. When I try doing either or I get a ExecJS::RuntimeError that says SyntaxError: unknown: Unexpected token.
This doesn't work:
array.push(<form>)
This works:
array.push(<form></form>)
JSX is syntactic sugar for creating tags in JavaScript using the React library.
<form></form>
in JSX compiles to:
React.createElement("form", null);
(Try it!)
JSX is not "HTML as a string" or whatever other way you might be thinking it works. It simply does not work the way you seem to want it to.
Without more detail about what exactly you want to achieve with this, we cannot help you further.
I ran into the same issue just now. I wanted to be able to add the separate tags so I can dynamically insert JSX tags or props in the middle. However, I used a work around for this.
You could potentially use functions to get similar functionality you want to insert items in between tags you wanted to split. For example, I wanted to put items in between the tbody and tr tags. I'm using ES6 syntax and Javascript classes here:
import React, {Component} from 'react';
class Table extends Component {
constructor(props)
this.powerLevels =
[
{level: "1", comment: "Weakling!"} ,
{level: "9001", comment: "It's over 9000!"}
];
}
//Adds columns in between the rows
addColumns = (index) => {
return (
<td> {this.powerLevels[index].level} </td>
<td> {this.powerLevels[index].comment} </td>
}
}
//Adds a table row
addRow = (index) => {
return (
<tr>
{this.addColumns(index)};
</tr>);
}
generateTable = () => {
let tableLength = this.powerLevels.length;
let jsxArray = [];
for(let i = 0; i < tableLength; i++){
jsxArray.push(this.addRow(i));
}
return (<tbody> {jsxArray} </tbody> );
}
render () {
//{} these brackets allow for Javascript to be written
return (
<div>
{this.generateTable()}
</div>
);
}
}

Categories

Resources