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>
);
Related
I have a situation where I need to parse a string, break, rejoin and then generate jsx from it. I'm somewhat successful but I have to make it clickable as well so that I can navigate on click but the problem is onClick remains a string even after conversion.
Here's the details:
I get a string in this format:
Some Text #(username)[a_long_user_id_to_navigate_to] some more text
I first get all the mentions from text and convert it to object of usernames with their IDs.
and then:
const createMessageWithActionableMentions = (message, mentions) => {
mentions.map(mention => {
message = message.replace('#'+'['+mention.username+']'+'('+mention.id+')',`<span style="color:${Colors.colorPrimary}; cursor: pointer" onClick={props.history.push('/some-route')}>#${mention.username}</span>`)
})
return message
}
the message argument in above function contains the raw string and the mentions is an array of objects as follows:
mentions = [
{
username: 'abcd_1234',
id: 'abcd_defg_hijk'
},
{
username: 'efgh_1234',
id: 'wxyz_defg_jklm'
}
...so on
]
Here's what I do when I add this text to view:
<p dangerouslySetInnerHTML={{__html: message}}></p>
Here's what I see on webpage:
Some text #username some more text
Here's what I see on Inspector:
<span style="color:#03F29E; cursor: pointer" onclick="{props.history.push('/some-route')}">#username</span>
Now the question is how do I handle this part onclick="{props.history.push('/some-route')} to work the react way. also, am I doing it correctly or there's a better way?
You can add click handlers to those spans using Vanilla JS as following:
Assign a class to those spans, for example user-mention
const createMessageWithActionableMentions = (message, mentions) => {
mentions.map(mention => {
message = message.replace('#'+'['+mention.username+']'+'('+mention.id+')',`<span style="color:${Colors.colorPrimary}; cursor: pointer" class="user-mention">#${mention.username}</span>`)
})
return message
}
Add event listeners to those spans. If you are using function components with hooks, put it inside a useEffect like this, and make sure this effect hook is called after the spans are appended to the DOM.
useEffect(() => {
[...document.getElementsByClassName('user-mention')].forEach((element) => {
element.addEventListener('click', () => {
props.history.push('/some-route');
})
})
}, [/* Your dependencies */])
Note that if the spans change (increase/decrease in number, for example), you need to handle event listeners when they change as well. If this becomes cumbersome, you can use Event delegation
You could do it like this:
const createMessageWithActionableMentions = (message, mentions) => {
// Will return a list of JSX elements
let output = [message];
// For every mention
mentions.map(
mention=>`#[${mention.username}](${mention.id})`
).forEach(mention => {
// For every string we have output so far
output = output.map(
substring => {
// Exclude existing JSX elements or strings that do not contain the mention
if (typeof substring!=="string" || !substring.includes(mention)) return [substring]
// We know the mention exists in this specific string segment so we need to find where it is so we can cut it out
const index = substring.indexOf(mention)
// Split the string into the part before, the part after, and the JSX element
return [
substring.substring(0,index),
<span style={{color:Colors.colorPrimary; cursor: "pointer"}} onClick={()=>props.history.push('/some-route')}>{mention.username}</span>,
substring.substring(index+mention.length)
]
}
// Reduce the nested array of arrays back into a single array
).reduce((a,b)=>[...a,...b], [])
})
return output
}
No Inner html shenanigens needed. Just use it like this:
<p>{message}</p>
I figured it out, this can be solved using the array, I simply first broken the message into array of substrings:
message = message.split(' ')
Then mapped it to find mentions in those substrings and replaced the respective substr with <span>:
message.map( (substr, index) => {
if(isMention(substr))){
found = mentions.find(mention => mention.username == substr.split('[').pop().split(']')[0])
if(found) {
message[index] = <span style={{color:Colors.colorPrimary, cursor: "pointer"}}>{found.username}</span>
}
}
})
The final code
const createMessageWithActionableMentions = (message, mentions) => {
let found = ''
message = message.split(' ')
message.map( (substr, index) => {
if(isMention(substr))){
found = mentions.find(mention => mention.username == substr.split('[').pop().split(']')[0])
if(found) {
message[index] = <span style={{color:Colors.colorPrimary, cursor: "pointer"}}>{found.username}</span>
}
}
})
return message
}
Then rendered it as jsx
<p>{message}</p>
I have an array of student objects that has some basic information like Name and Address. I want to be able to add a tag property to the object that is based on the user's input
I am acomplishing this with an input like this
<input placeholder="new tag" onInput={(e) => setAddedTag(e.target.value)} />
<button type="submit" onClick={() => addTag()}>Add Tag</button>
And I am adding the tag property to the specific object with this code
const addTag = () => {
setStudentIndex(students.id - 1)
students[studentIndex].tag = [AddedTag]
// needs to add more than 1
}
However this seems to only work with one tag, and if the user adds a second tag it will overwrite the first one. (or it will just crash)
I have tried using the spread operator
students[studentIndex].tag = [...AddedTag]
However this instead set the tag to be ['a', 'b', 'c'] when the user had typed in abc
How can I accomplish adding an array of string as a prop?
have you tried using push()? something like:
students[studentIndex].tag.push(AddedTag);
define the tag to be an array within the object. Something like this:
const students = [
{
name: "xyz",
address: "abc",
tag: []
}
]
Then in your code change the following line from:
students[studentIndex].tag = [AddedTag]
to students[studentIndex].tag.push(AddedTag)
That should do it.
try
const { tag = [] } = students[studentIndex]
students[studentIndex].tag = [...tag, AddedTag]
I have an array of objects called 'Tags' of type :
type Tag = {
id: number,
label: string
}
Below is some sample data inside it.
const [tags, setTags] = useState<Tag[]>([{id: 1, label: "random"}, {id: 2, label: "important"}, {id: 3, label: "ignore"}])
I have an input field which takes input and sets it to "input" state on change.
I want to display a paragraph element only if the searched input field doesn't exist inside the tags.
I was unable to find a way to directly search it as the tags array is made of objects and i want to search that object's label property.
So i ended up doing something like this...
{tags.map(tag => {
if(!(tag.label.toLowerCase().includes(input.toLowerCase()))){
return <p>{input}</p>
}
})}
but this would render the paragraph each time the input doesn't match the values. So in this case it renders the paragraph 3 times if i add a new label. I want it to render just once. How do i fix this?
You could use filter to filter tags and then if result has a length > 0 show <p>{input}</p> one time:
return (
{tags.filter(tag => !(tag.label.toLowerCase().includes(input.toLowerCase())).length > 0 &&
<p>{input}</p>
})});
To render the input in a p tag when none of the labels includes it:
const Bla = () => { // assuming Bla is your component
const shouldRenderInput = tags.every(
(tag) => !tag.label.toLowerCase().includes(input.toLowerCase())
);
return <>{shouldRenderInput && <p>{input}</p>}</>;
};
see Array.prototype.every
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]}/>
}
Here is some sample text:
"-A3-<tag1>Sale</tag1>-cum-</blockquote><p <tag1>id</tag1>=\"p_12\"> </p><p
<tag1>id</tag1>=\"p_13\"> Gift <tag1>Deed</tag1> <tag1>executed</tag1> by C.K. Koshy
<tag1>in</tag1> favour <tag1>of</tag1> Quilon Diocese <tag1>and</tag1> C.K."
I was wondering how can I remove the <p> tags, the <blockquote> tags as well as all the id but, I want to keep the <tag>. Only if the <tag> is wrapping around the p, blockquote or the id I want to remove it. Rest all the time it stays. How do I do it in JavaScript? I tried regex with the following pattern:
test = test.replace(/<(?!tag\s*\/?)[^>]+>/g, '')
This did not yield satisfactory results. This is what I am aiming for (The desired output):
"-A3-<tag1>Sale</tag1>-cum- Gift <tag1>Deed</tag1> <tag1>executed</tag1> by C.K. Koshy
<tag1>in</tag1> favour <tag1>of</tag1> Quilon Diocese <tag1>and</tag1> C.K."
How do I get this is JS?
You can parse the string as HTML and use DOM manipulation to remove the elements, here is an example:
let input = `-A3-<tag1>Sale</tag1>-cum-</blockquote><p <tag1>id</tag1>="p_12"> </p><p
<tag1>id</tag1>="p_13"> Gift <tag1>Deed</tag1> <tag1>executed</tag1> by C.K. Koshy
<tag1>in</tag1> favour <tag1>of</tag1> Quilon Diocese <tag1>and</tag1> C.K.`
input = input.replace(/<tag1>id<\/tag1>/g, "id");
const doc = new DOMParser().parseFromString(input, 'text/html');
doc.querySelectorAll('blockquote, p').forEach((e) => {
[...e.childNodes].forEach(child => {
e.before(child);
});
e.remove();
});
const output = doc.body.innerHTML;
console.log(output);
What this does is to first use some regex to make the original input into valid HTML and then go through all the <blockquote> and <p> elements to extract all the child nodes and insert them before their parents, once that is done, the empty <blockquote> and <p> elements are removed.
When dealing with HTML (or any XML), regex is never the answer. There are way too many gotchas.
Instead, you'll want something like jQuery or Cheerio (which mimics jQuery's API but just contains the stuff for transforming and manipulating HTML without the extra stuff) which understands the actual structure. If you trust the content, you could also dump the HTML into an HTML element with innerHTML and access it with querySelector().
Then it is just a matter of selecting the element(s) you don't want and deleting them.
Here is an example using the third option:
const content = '<p>a</p><p>b</p><tag>c</tag><tag><p>d</p></tag><blockquote>e</blockquote>';
const dummy = document.querySelector('#dummy');
const output = document.querySelector('#output');
dummy.innerHTML = content;
const toRemove = Array.from(dummy.querySelectorAll('p, tag, blockquote')).filter(el =>
// for the "element containing certain element" you can't do that with selector along, but can with a filter method
!(
(el.tagName === 'P' && el.parentNode.tagName === 'TAG')
|| (el.tagName === 'TAG' && el.querySelector('p'))
)
);
toRemove.map(el => el.parentNode.removeChild(el));
output.innerHTML = dummy.innerHTML;
dummy.innerHTML = ''; // empty it out
<div id="dummy"></div>
<div id="output"></div>