How can I build HTML gradually in React? - html

I am trying to build some HTML in the following format:
<h1>Title</h1>
<p>
Always existing sentence. Optional sentence.<br/>
Always existing sentence. Optional sentence.
</p>
<p>
<span>Always existing span</span>
<span>Optional span</span>
</p>
What is the best way to construct/render this?
The nearest I have got is some JSX:
const title = <h1>Title</h1>
const line1 = [`Always existing sentence.`];
const line2 = [`Always existing sentence.`];
if (condition) {
line1.push(`Optional sentence.`);
line2.push(`Optional sentence.`);
}
const para1 = React.createElement('p', null, [line1.join(' '), line2.join(' ')].join('<br/>'));
const spans = [React.createElement('span', null, `Always existing span`)];
if (condition2) {
spans.push(React.createElement('span', null, `Optional span`));
}
const para2 = React.createElement('p', null, spans.join('<br/>'));
return <>{title}{para1}{para2}</>;
This works apart from the final paragraph which renders as <p>[object Object]<br/>[object Object]</p>. I assume because the spans are ReactElements. But I don't know how to join() them. Or even if this is the best approach.
So, what is the best way to gradually build up HTML to render in React?

I believe what you're looking for is conditional rendering of JSX. You have the right idea with your pseudocode, you just need to change that if (condition) into checking for the value of a state variable. It's unclear if you're using the class syntax or hooks, so I'll provide an example using hooks for simplicity. The same idea applies regardless of which syntax you use.
Whenever state changes in React, the component will be re-rendered. With showOptionalContent set to false by default, the optional content will not be displayed. When showOptionalContent is changed to true, the component will re-render and the optional content will be displayed. You can use lifecycle methods or the useEffect hook to call a method that changes the state depending on when/why you want the optional content to be rendered.
const [showOptionalContent, setShowOptionalContent] = setState(false);
return (
<div>
<h1>Title</h1>
<div>Always rendered content</div>
{showOptionalContent && ( // only renders when showOptionalContent === true
<div>Optionally rendered content</div>
)}
<span>Always rendered content</span>
{showOptionalContent && ( // only renders when showOptionalContent === true
<span>Optionally rendered span</span>
)}
</div>
)

I ended up doing the following in order to achieve what I set out to:
const title = <h1>Title</h1>
const line1 = [`Always existing sentence.`];
const line2 = [`Always existing sentence.`];
if (condition) {
line1.push(`Optional sentence.`);
line2.push(`Optional sentence.`);
}
const para1 = <p>{line1.join(' ')}<br/>{line2.join(' ')}</p>;
//This is a huge span
const span1 = <span>Always existing span.</span>;
let span2 = null;
if (condition2) {
//This is a huge span
span2 = <span>Optional span.</span>;
}
const para2 = <p>{span1}{span2 && (<><br/>{span2}</>)}</p>;
return <>{title}{para1}{para2}</>;
Still seems a bit unclean, but it works and it was the best I could come up with.

Related

Add "+5" if there is no space for more elements

I have a div (ItemsContainer) that has an array of items (Item) being rendered inside of it. The div has a dynamic size, depending on the size of the screen.
As I'm mapping through the array, I'd like to be able to make a check to see if there is enough space to render the current item. If there isn't, I'd like to stop rendering the items and instead add another item that says "+(number of items not rendered in array)". See the included picture for reference.
So far, this is what my code looks like. I'm using React typescript. I haven't attempted adding the "+5" box yet, because I'm wondering if it's actually possible? My initial thought is to just have a fixed number of items be displayed and then display the + item if there are more items not rendered, but I was hoping you could do it a bit more dynamic.
const Items: FC<Props> = ({ items }) => {
return (
<ItemsContainer>
{items.map((item, index) => (
<Item key={index}>{item.name}</Item>
))}
</ItemsContainer>
);
};
Here is a partially working solution: https://codepen.io/Latcarf/pen/WNKZmBN.
The main part of the code is the following:
const Tags = ({tags}) => {
const [hiddenCount, setHiddenCount] = React.useState(0)
const shownTags = hiddenCount == 0 ? tags : tags.slice(0, -hiddenCount);
const ref = React.useRef();
React.useLayoutEffect(() => {
if (!ref.current) {
return;
}
if (ref.current.scrollWidth > ref.current.clientWidth) {
setHiddenCount(count => count + 1);
}
});
return (
<div class="container" ref={ref}>
{shownTags.map(tag => <div class="tag">{tag}</div>)}
{hiddenCount > 0 && <div class="tag">{`+${hiddenCount}`}</div>}
</div>
)
};
The idea is to keep a hiddenCount state variable that contains the number of hidden tags. It starts at 0, and then a layout effect checks whether there is space and keeps incrementing it until there is enough space. It's a bit inefficient if you have many hidden items, as it will rerender many times until it reaches the correct amount, but it should properly deal with edge cases like if adding +1 would actually be longer than displaying the last tag, or stuff like that.
In order to make it update automatically on resize, you would use some kind of useResizeObserver hook to reset hiddenCount to 0 after a resize. Somehow I couldn’t manage to import other packages in CodePen, so I just made a button that forces a rerender so that you can test it (resize the div and then click the button).

Apply CSS only for number in React

If I have JSX like
<div>Hello there are 123 oranges</div>
Here's how text is set.
const Component = ({text}) => {
return(
<div>{text}</div>
)
}
I wanna make 123 red color. However, the text inside div is dynamic, so I can't insert span by hardcoding. Hopefully CSS can detect numeric but I couldn't find one by googling.
Are there any good and concise solutions?
It 's possible with dangerouslySetInnerHTML, you need regex to match the numeric part from your dynamic string and then generate the updated string with a style wrapped around that numeric part.
const yourText = yourText.replace(/\d+/, m => `<span style="color: red">${m}</span>` );
then pass this to dangerouslySetInnerHTML.
<div dangerouslySetInnerHTML={{__html: yourText}}></div>
You can perhaps try with regex
text.replaceAll(/(\d+)/g,'<span className="num">$1</span>')
String.prototype.replaceAll is an ES10 method.

footer.replaceText() does not replace my placeholder with new text

Can anyone explain why footer.replaceText() does not replace my placeholder with new text. It works for both header and body.
//opens the master report document which is now saved as the student's name and gets the contents of the header
let header = DocumentApp.openById(documentId).getHeader()
//opens the master report document which is now saved as the student's name and gets the contents of the body
let body = DocumentApp.openById(documentId).getBody()
//opens the master report document which is now saved as the student's name and gets the contents of the footer
let footer = DocumentApp.openById(documentId).getFooter()
header.replaceText('{{First Name}}', row[studentDetails.firstName])
body.replaceText('{{Maths Attainment}}', row[studentDetails.mathsAttainment])
footer.replaceText('{{Class Teacher}}', row[studentDetails.classTeacher])
I can't seem to find an answer on Stack Overflow that works.
If you're using the option 'Different first page' you can't get the footer and header of the first page in the common (documented) way.
Based on this answer try to get them this 'secret' way:
let first_header = header.getParent().getChild(3);
let first_footer = header.getParent().getChild(4);
And then you can change them as usual:
first_header.replaceText('{{First Name}}', row[studentDetails.firstName]);
first_footer.replaceText('{{Class Teacher}}', row[studentDetails.classTeacher]);
I would like to give credit to Mr. Amit Agarwal who has provided a working solution to above.
The code is below:
const replaceHeaderFooter = () => {
// Returns the document with the specified ID
const doc = DocumentApp.openById('DOCUMENT ID');
// Retrieves the headers's container element which is DOCUMENT
const parent = doc.getHeader().getParent();
for (let i = 0; i < parent.getNumChildren(); i += 1) {
// Retrieves the child element at the specified child index
const child = parent.getChild(i);
// Determine the exact type of a given child element
const childType = child.getType();
if (childType === DocumentApp.ElementType.HEADER_SECTION) {
// Replaces all occurrences of a given text in regex pattern
child.asHeaderSection().replaceText('{{Company}}', 'Digital Inspiration');
} else if (childType === DocumentApp.ElementType.FOOTER_SECTION) {
// Replaces all occurrences of a given text in regex pattern
child.asFooterSection().replaceText('{{Copyright}}', '© Amit Agarwal');
}
}
// Saves the current Document.
// Causes pending updates to be flushed and applied.
doc.saveAndClose();
};
Thank you!

How to insert HTML inside template literal strings?

In React, I want to be able to use style words within a string which is defined in a variable using template literals.
For that I am making use of a to just style that word.
I am getting HTMLIntrinsic usage error.
Note- Solutions given in SO to questions related to this does not solve the issue I have. Pls check the code.
How to circumvent this problem
Tried using dangerouslyinsertHTML, but not a recommended solution.
//Actual code
const temperature = "22";
const list = {
item: `The temperature is ${temperature}`
}
//To style it-
const temperature = "22";
const list = {
item: `The temperature is <span style={{color:'red'}}>${temperature}</span>`
}
//And the above list.item is inserted inside JSX like -
return (
<div>{list.item}</div>
)
The temperature(22) needs to be styled.
Instead of the template string, you can use JSX elements for generating HTML as usual, placed next to your text elements. Example:
item: (
<>
The temperature is
<span style={{color:'red'}}>
{temperature}
</span>
</>
)
I'm using a Fragment to wrap the text and elements together, but you can use something else like a div if you wish to style the wrapper too.
You can't use React in template-literal like that because the React component is an object. Using it with template-literal will result in this[object Object] . So I recommend use other way, for example the solution by #richardo
You can make this as simple as this, IF you are OK to not have object like you defined
return(
<div>The temperature is <span style={{color: 'red'}}>{temperature}</span></div>
)

xul-Get selection html

I have the following function that is supposed to get HTMLs for the user selected area on the web page. This function does not seems to work properly.
Sometime, it gets htmls which is not selected also.
Can anyone please look into this function? -- Thanks a lot.
//----------------------------Get Selected HTML------------------------
function getSelectionHTML(){
if (window.getSelection)
{
var focusedWindow = document.commandDispatcher.focusedWindow;
var sel = focusedWindow.getSelection();
var html = "";
var r = sel.getRangeAt(0);
var parent_element = r.commonAncestorContainer;
var prev_html = parent_element.innerHTML;
if(prev_html != undefined)
{
return prev_html;
}
return sel;
}
return null;
}
It looks to me like you're getting the contents of the parent element rather than the selection itself. If the parent element contains anything other than what you have selected, then you'll get that too.
var sel = focusedWindow.getSelection();
This line returns a selection object. It contains the exact text selected by the user. You then get the range from the selection and get the commonAncestorContainer. So if you have code like this:
<div id="ancestor">
<p>First sentence.</p>
<p>Another sentence.</p>
</div>
And your user selects from the 's' of the first sentence to the 's' of the second sentence then the commonAncestorContainer is the div element so you'll also get the rest of the text.
A good reason for this would be if you wanted to guarantee yourself a valid HTML fragment (this seems to be the case, implied by your function name), but if you just want the selected text then call the toString method on the range directly:
var focusedWindow = document.commandDispatcher.focusedWindow;
var sel = focusedWindow.getSelection();
var r = sel.getRangeAt(0);
return r.toString();