Highlight part of Text - html

I was building project similar to Google Search using React.js and GoogleAPI.
Here I have got a problem.
For example, if you search "tea" on Google, you can see "Searches related to tea" at the bottom of the result, just above the pagination.
There, the words expect "tea" is bold.
How can I implement it using React?
I know the word "tea" and full text of related searches, but how can I highlight the part of text?
Many help.
Here is my code.
In the code, item.query is the full text such as "black tea", and searchValue is "tea". I just want to make "black" bold.
import React, { Component } from ‘react’;
import { connect } from ‘react-redux’;
class RelatedSearch extends Component {
render() {
var res = this.props.searchResults.related_searches;
return (
<div>
<br/><br/>
<p>Related Search</p>
{res.map((item, index) =>
<Item item={item} key={index} />
)}
</div>
);
}
}
function Item (item, key) {
return <div>{item.item.query}</div>;
}
const mapStateToProps = state => ({
searchResults: state.usersReducer.searchResults,
searchValue : state.usersReducer.searchValue,
});
export default connect(mapStateToProps)(RelatedSearch);
I have got answers here and already accepted it, but I received suggestion not to use dangerous html. Is there any other solution?

Here is what I came up with (it may not work in all edge cases but I tested it and it works in most situations)
First get a handle on the search term
let searchTerm = "Hello";
Then create a function that loops through the current string
createHighlight(text) {
// split the string at the point(s) where the search term is present.
let split = text.toLowerCase().split(searchTerm.toLowerCase());
// create a placeholder string.
let ttt = "";
// loop through the splited string and put in the search term after each one and wrap it in a span with a class 'highlight' unless it is the last one.
for (let i = 0; i < split.length; i++) {
if (i === split.length - 1) {
ttt += split[i];
} else {
ttt += `${split[i]} <span class="highlight">${searchTerm}</span>`;
}
}
//return the string as HTML.
return ttt;
}
Use it in your HTML (in place of just inserting the string)
<p dangerouslySetInnerHTML={{
__html: this.createHighlight("hello here hello is text hello")}}
/>
And remember to add a class to style the highlighted text (.highlight {bacround-color: yellow})
Here is a link to a codepen https://codesandbox.io/s/flamboyant-frost-hn47k?fontsize=14

Using javascript
var searchTerm = "hello";
function heighlightText() {
var ele= document.getElementById("sample");
var arrText = ele.innerHTML.split(searchTerm);
var rst = "";
for(i=0;i<arrText.length;i++) {
if(i === arrText.length - 1)
{
rst +=arrText[i];
continue;
}
rst +=arrText[i] + "<span class='highlight'>" + searchTerm +"</span>";
}
ele.innerHTML = rst;
}
heighlightText();
.highlight {
background-color : yellow;
}
<div id="sample">hello test search the things hello and then</div>

You can try this, see if it helps:
class App extends React.Component{
highlightText(sentence, wordToHighlight){
let highlightedText = sentence.split(" ")
.map(word => word.toUpperCase() === wordToHighlight.toUpperCase() ? `<b>${word}</b>` : word)
.join(" ");
return {__html: highlightedText};
}
render(){
return(
<div dangerouslySetInnerHTML={this.highlightText("I am a robot charlie.", "robot")} />
)
}
}
ReactDOM.render( <App />, document.getElementById('root') );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />

Related

HTML doesn't update properly on child element after sorting an array of items on parent component when using updated()

I am sorting an array of so called 'activities' in my customElement using LitElement:
#property({ type: Array }) private activityListLocal: Array<Activity> = [];
in the parent customElement called "find-activity".
Each activity is being rendered here.
${repeat(
this.activityListLocal,
activity =>
html` <div class="activity-container">
<div class="activity">
<activity-info .activity=${activity}></activity-info>
</div>
<div class="activity" class="rating">
<activity-rating
.activity=${activity}
#appactivityremoveclick=${() => this.deleteActivity(activity)}
></activity-rating>
</div>
</div>`
)}
This is how it looks visually:
2 activities marked for some impression
On clicking the button "Highest Rating", I am sorting the list of activities:
sortActivityListLocal() {
this.activityListLocal = [...this.activityList];
this.activityListLocal = this.activityListLocal.sort((a, b) => (a.avgRating < b.avgRating ? 1 : -1));
}
if (category === 'all') {
this.activityListLocal = this.activityList;
} else if (category === 'Highest Rating') {
this.sortActivityListLocal();
if (this.activityListLocal.length === 0) {
this.nothingHere.style.display = 'block';
}
}
//....
}
Note: this.activityList is a local copy of the server response.
In the image, you see the two sliders, which should be updated to move with the activity if the position on the page changes. The issue: The "my rating" slider does not properly "move" with the activity, if it has been changed/dragged after the page has been loaded for the first time.
Before:
Activities are loaded in properly, cinema has a higher rating than Tennis
After:
Activities are sorted properly, all sliders are correctly "moved" if "myRating" has not been changed/dragged
But if the slider was dragged after inital load in, and then selecting the "highest rating" category and therefore sorting the array, it stays in place:
Before:
After loading
Dragging the slider (not even requesting an update with a click on the refresh icon, issue happening in both cases)
Modification leading to the issue
After:
Issue visible
The interesting thing, the slider has the correct! value in the html inspector, but the display is not showing it. Why is this happening?
Code of the component holding the sliders:
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { httpClient } from '../../../http-client';
import { PageMixin } from '../../page.mixin';
import { Activity, Rating } from '../find-activity';
import componentStyle from './activity-rating.css';
#customElement('activity-rating')
// eslint-disable-next-line #typescript-eslint/no-unused-vars
class ActivityRatingComponent extends PageMixin(LitElement) {
static styles = componentStyle;
#property({ reflect: true }) activity = {} as Activity;
#property() rating = {} as Rating;
#query('#deleteButton') private deleteButton!: HTMLImageElement;
private currentSliderValue = -1;
async updated() {
console.log(
`Personal rating for ${this.activity.title} is ${this.activity.personalRating}, avgRating ${this.activity.avgRating}, currentSliderValue ${this.currentSliderValue}`
);
this.currentSliderValue = this.activity.personalRating ? this.activity.personalRating : 0;
console.log(`Current slider value after: ${this.currentSliderValue}`);
if (this.activity.deletepermission === false) this.deleteButton.style.display = 'none';
else this.deleteButton.style.display = 'inline';
}
render() {
return html`
${this.renderNotification()}
<div class="outer-rating">
<p>${this.activity.motivationtitle}</p>
<div class="slidecontainer">
<label for="overallRating">Overall Rating</label>
<input
type="range"
min="0"
max="100"
value=${this.activity.avgRating ? this.activity.avgRating : 0}
class="slider"
id="overallRating"
disabled
/>
</div>
<div class="slidecontainer">
<label for="myRating">My Rating</label>
<input
type="range"
min="0"
max="100"
value=${this.activity.personalRating ? this.activity.personalRating : '0'}
class="slider"
id="myRating"
#change="${(e: Event) => this.readcurrentSliderValue(e)}"
/>
<img id="personalSlider" src="/refresh.png" alt="update" #click=${this.savecurrentSliderValueToDb} />
<img
class="remove-task"
src="/deleteicon.png"
alt="update"
id="deleteButton"
#click="${this.confirmDelete}"
/>
</div>
</div>
`;
}
confirmDelete(e: Event) {
const target = e.target as HTMLInputElement;
if (target) {
const result = confirm('Want to delete?');
if (result) {
this.emit('appactivityremoveclick');
}
}
}
readcurrentSliderValue(e: Event) {
const target = e.target as HTMLInputElement;
if (e) {
this.currentSliderValue = Number(target?.value);
console.log('Read new slider value ' + Number(target?.value));
}
}
async savecurrentSliderValueToDb() {
const partialRating: Partial<Rating> = {
activityid: this.activity.id,
rating: Number(this.currentSliderValue) //userID is not included here as it is being provided by the auth Middleware on patch request.
};
await httpClient.patch(`rating/${this.activity.id}${location.search}`, partialRating);
const responseRatingAll = await httpClient.get(`rating/findAverageRating/${this.activity.id}` + location.search);
try {
this.activity.avgRating = (await responseRatingAll.json()).results;
this.activity.personalRating = partialRating.rating ? partialRating.rating : 0;
} catch (error) {
this.showNotification((error as Error).message, 'error');
}
this.requestUpdate();
}
emit(eventType: string, eventData = {}) {
const event = new CustomEvent(eventType, {
detail: eventData,
bubbles: true,
composed: true
});
this.dispatchEvent(event);
}
}
Visual confirmation that slider has the correct value, but doesn't show it.
Thank you :)
Edit: In addition to the answer below - specifically for the case where "you want to force a value to be set on an element". Lit has an optimization where "if a value hasn't changed, don't do anything". Rendering the same value to an expression will not cause the expression to update. To make sure Lit updates the expression if the underlying DOM value has changed use the live directive.
The native browser input elements default behavior is:
When the value attribute is changed, update the input elements value property.
After a manual user interaction (such as typing into the input element if it is a text input), the value attribute no longer updates the input property.
After the value property has been updated the attribute no longer causes the property to update.
Therefore by setting the value property the value updates.
Because of that browser behavior, in Lit you can use a property expression to set the value property.
I.e.: <input .value=${this.activity.avgRating ? this.activity.avgRating : 0}.
Below is an example of the browser input behavior. Click the two buttons. One will update the value attribute, the other the value property.
Then interact with the input. Type in it. Now the attribute button will stop working.
const inputEl = document.querySelector('input')
const getRandomValue = () => String(Math.random()).slice(0, 5)
document.querySelector("#btn-attr")
.addEventListener("click", () => {
inputEl.setAttribute('value', getRandomValue())
});
document.querySelector("#btn-prop")
.addEventListener("click", () => {
inputEl.value = getRandomValue()
});
<input value="12345">
<button id="btn-attr">Change input attribute</button>
<button id="btn-prop">Change input property</button>

html strings array convert to JSX

`import React from 'react'
export default function Quiz(props){
// generate random index without duplicates
function generateRandomIndex(){
const randomNumArr=[]
for (var a = [0, 1, 2, 3], i = a.length; i--; ) {
var random = a.splice(Math.floor(Math.random() * (i + 1)), 1)[0];
randomNumArr.push(random)
}
return randomNumArr
}
let randomNumbers = generateRandomIndex()
let spreadOptions = ()=>{
let optionsHtmlArray = []
for(let i=0; i<props.answers.length; i++){
optionsHtmlArray.push(`<span className='answers' key=${i} style={${{backgroundColor: props.correct===props.answers[i] ? "green" : "red"}}}>
{ ${props.answers[i]} } </span>`)
}
return optionsHtmlArray
}
return (
<div className='Quiz'>
<h3 className='question'>{props.question}</h3>
<div className='answers_div'>
{ spreadOptions()[randomNumbers[0]] }
{ spreadOptions()[randomNumbers[1]] }
{ spreadOptions()[randomNumbers[2]] }
{ spreadOptions()[randomNumbers[3]] }
</div>
<hr className='hr'/>
</div>)
}
'
'//this is from App.js
// fetch to API when first render to save data to the state,
// and fetch depending on the sate of showOverlay
React.useEffect(() => {
fetch("https://opentdb.com/api.php?amount=5&category=9&difficulty=easy&type=multiple")
.then(res => res.json())
.then(data => {
setQuestions(data.results)
//after set questions state that comes from fetch request
//and set the custom questions with some properties I need
setCustomQuestions(prevQuestions=>{
let newArr=[]
for(let i=0; i<data.results.length; i++){
newArr.push({question: data.results[i].question,
questionId: nanoId(),
answers: [data.results[i].correct_answer].concat(data.results[i].incorrect_answers),
correct: data.results[i].correct_answer})
}
return newArr
})
})
}, [])
// Quiz component properties
const customQuestionsArr = customQuestions.map(question => {
return < Quiz
key={question.questionId}
question={question.question}
answers={question.answers}
correct={question.correct}
/>
})'
Hi all, I am trying to render all options of the answers in Quiz component, however,
spreadOptions() returns an array of html strings for the answers
I gotta parse to JSX to make it work.
I tried to install react-html-parser, didn't work it only gave me a bunch of error every time when I try to install dependencies through npm
I tried dangerouslySetInnerHTML, but also didn't work
Would you be able to provide the props that you are trying to pass to Quiz component?
Below is a snippet of code with modified spreadOptions and jsx. I wasn't able to test this code tho but will update it if you can provide the sample props.
let spreadOptions = props.answers.map((a, i) => (
<span
key={i}
className='answers'
style={{
backgroundColor: props.correct === a ? 'green' : 'red',
}}
>
{a}
</span>
));
return (
<div className="Quiz">
<h3 className="question">{props.question}</h3>
<div className="answers_div">
{spreadOptions}
</div>
<hr className="hr" />
</div>
);

why does it take two clicks for the DOM view to appear

0
this is my code now
template file:
<quill-view-html [content]="question.questionText" format="html" theme="snow" [id]="question.id" [change]="loadAnswer(studentAnswer)"
.ts file:
loadAnswer(studentAnswer) {
if (document.getElementById(question.id)) {
let inputs =
document.getElementById(question.id).getElementsByTagName('input') || [];
inputs.forEach((input, index) => {
input.value = studentAnswer[index]
})
}
}
the answer gets display in the input box but i am not able to type a new answer over it since the previous answer gets coming back. Any solution to this?
Firstly, let's make your code more pretty.
loadAnswer(studentAnswer){
if (document.getElementById(question.id)) {
let inputs = document.getElementById(question.id).getElementsByTagName('input') || [];
inputs.forEach((input, index) => {
input.value = studentAnswer[index]
})
}
}
Can you share your template code with us? Function looks good.

Filter search results react

My data back from search result has columns: enTitle,Image,url,enDescription,HasLandingPage,AddInfo.
I want to filter search results by AddInfo to show in different lists. later if I can add a button that would be better.
Render Data:
const ListArticle = (props) =>{
return (
<div className="card">
<div className="search-img-lft">
<a href={props.link} target="_blank">
<img src={props.image} alt="" />
</a>
</div>
<div className="search-imgcont-rgt">
<a href={props.link} target="_blank">
<h3>
{props.title}
{props.kind} // just to see if kind works
</h3>
<p>{props.desc}</p>
</a>
{props.link}
</div>
</div>
);
}
List Class:(ignore the i,brac & lim they are for pagination)
class List extends React.Component {
render(){
const liArt =[];
const searchText = this.props.searchText.toLowerCase().replace(/[^a-z0-9]/g, '');
var i = 0;
const brac = this.props.start;
const lim = brac + this.props.qtyPerPage;
//the filter below works for resources but I want all to be filtered and show in the list in previous code snippet
this.props.list.filter(u=>u.AddInfo == "resource").map((article)=>{
var artText = (article.enTitle + " " + article.URL + " " + article.enDescription + " " + article.AddInfo).toLowerCase().replace(/[^a-z0-9]/g, '');
if(artText.indexOf(searchText)===-1){
return;
}
i++;
if(brac<i && i<lim){
liArt.push(
<ListArticle key={article.Image+article.URL}
title={article.enTitle}
image={article.Image+"?h=100&mode=crop&scale=down"}
link={JSON.stringify(article.HasLandingPage).toUpperCase()=="TRUE" ? "/en/"+article.URL : "/" + article.URL}
desc={article.enDescription}
kind={article.AddInfo.includes("SKU") ? " Product" : (article.AddInfo.includes("resource") ? " Resource" : " Page")} />
);//push
} //limit check
});//map
return (
<div className="search-page-listbox">
{liArt}
</div>
);
}
}
If i got you right, you want to create multiple lists while each list shows items of another"AddInfo".
First, I would recommend to separate your task into three components (instead of two):
First component is the ListArticle which will be the list item,
Second will be the component List -> that will receive the list you want to show (after they have been filtered),
Last component will be ListContainer -> this one will hold multiple lists (as many as the options of AddInfo).
Then, in ListContainer you can go over all unique AddInfo, and create List component for every option - passing only filtered items:
ListArticle.js
import React from 'react';
const ListArticle = (props) =>{
return (
<div className="card">
<div className="search-img-lft">
<a href={props.link} target="_blank">
<img src={props.image} alt="" />
</a>
</div>
<div className="search-imgcont-rgt">
<a href={props.link} target="_blank">
<h3>
{props.title}
{props.kind} // just to see if kind works
</h3>
<p>{props.desc}</p>
</a>
{props.link}
</div>
</div>
);
}
export default ListArticle;
List.js
import React from 'react';
import ListArticle from './ListArticle';
export default class List extends React.Component {
render() {
return (
this.props.list.map(article => <ListArticle key={article.Image + article.URL}
title={article.enTitle}
image={article.Image + "?h=100&mode=crop&scale=down"}
link={JSON.stringify(article.HasLandingPage).toUpperCase() == "TRUE" ? "/en/" + article.URL : "/" + article.URL}
desc={article.enDescription}
kind={article.AddInfo.includes("SKU") ? " Product" : (article.AddInfo.includes("resource") ? " Resource" : " Page")} />
)
)
}
}
ListContainer.js
import React from 'react';
import List from './List';
class ListContainer extends React.Component {
constructor(props){
super(props);
}
render() {
let lists = {};
let searchText = this.props.searchText;
if(this.props){
let filteredList = this.props.list.filter(article=>(article.enTitle + " " + article.URL + " " + article.enDescription + " " + article.AddInfo).toLowerCase().replace(/[^a-z0-9]/g, '').indexOf(searchText)!==-1);
filteredList && filteredList.forEach(u => {
if(lists[u.AddInfo]===undefined) lists[u.AddInfo]=[];
lists[u.AddInfo].push(u);
});
}
return(
Object.keys(lists).map(function(key, index) {
return <List list={lists[key]} />
})
)
}
}
export default ListContainer;
Usage:
<ListContainer list={list} searchText={searchText} />
Hope it helped :)
I'd return something like this from your List class (I tried explaining my thought process in comments inside the code):
return (<React.Fragment>
{
// second (kinda), I'd convert the inside generated collection (object) into an array
// -> where the array elements are now ["AddInfo type", [array of elements with that type]]
Object.entries(
// first, convert the list into an object, collecting each type of "AddInfo" into
// -> a unique property, and storing all objects w/ that type in an array
this.props.list.reduce((output, u) => {
if (!output[u.AddInfo]) output[u.AddInfo] = [];
output[u.AddInfo].push(u);
return output
}, {})
)
// third, I'd map the new array of ["AddInfo type", [array of elements with that type]] into
// -> the jsx output you want, like this:
.map(([type, array]) => {
return <div className="search-page-listbox">
{array.map((article, i) => {
// ... everything inside your ".map((article...)" function can go here
})}
</div>
})
}
</React.Fragment>)
A few notes:
You can replace var i = 0 and i++ lines with the i index that automatically comes from the second parameter in the map function (see my version of array.map((article, i) => ...)
If you haven't seen things like array destructuring (ex: .map(([type, array]) => ...)) let me know, I can explain. It's a pretty shnazzy thing you can do to save some lines.
My first step was to figure out how to create an object container which holds sorted data based on AddInfo - that's why my // first comment comes technically after the // second comment. Hope that makes sense.
Let me know if you have questions or if there was a typeo that's breaking my code. I haven't tested it obviously since I don't have your react code / variables.

Why are my React rendered HTML elements changing positions after refresh?

I have a react component that I am using as checkpoint to check if the user has viewed a certain section of the site.
class ContentSection extends Component {
constructor(props) {
super(props);
}
render() {
const allParagraphs = [];
for (let i = 0; i < this.props.paragraphs.length; i++) {
let p = this.props.paragraphs[i];
allParagraphs.push(
<Paragraph key={i} image={p["img"]} text={p["text"]} />
);
}
return (
<div className="cs">
<ContentSectionCheckPointContainer
uniqueIndex={this.props.uniqueIndex}
/>
<h4 className="sectionTitle">THIS IS A SECTION!!!</h4>
{allParagraphs}
</div>
);
}
}
And this is the ContentSectionCheckPointContainer
const mapDispatchToProps = dispatch => {
return {
unlock: index => dispatch(Unlock_Index_Action(index))
};
};
const mapStateToProps = state => {
return {
CheckPoints: [...state.CheckPoints]
};
};
class ContentSectionCheckPoint extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.myRect = null;
this.checkVisibility = this.checkVisibility.bind(this);
}
componentDidMount() {
this.checkVisibility();
window.addEventListener('scroll', this.checkVisibility);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.checkVisibility);
}
checkVisibility() {
if (this.myRef.current) {
let rect = this.myRef.current.getBoundingClientRect();
var viewHeight = Math.max(
document.documentElement.clientHeight,
window.innerHeight
);
let b = !(rect.bottom < 0 || rect.top - viewHeight >= 0);
if (b !== this.props.CheckPoints[this.props.uniqueIndex]) {
if (b) {
this.props.unlock(this.props.uniqueIndex);
}else{
this.props.unlock(this.props.uniqueIndex);
}
}
}
}
render() {
this.checkVisibility();
return (
<div ref={this.myRef} className="cscp">
{this.props.CheckPoints[this.props.uniqueIndex] && <p>hi</p>}
</div>
);
}
}
const ContentSectionCheckPointContainer = connect(
mapStateToProps, mapDispatchToProps)(ContentSectionCheckPoint);
As you can see I ran a visibility check on scroll, which works fine. However, I wanted to also run the visibility check immediately when the page is loaded before any scrolling occur.
It is to my understanding that componentDidMount is when React already rendered an element for the first time, so I wanted to do the check then. However, I was trying to render two ContentSection components, each containing their own check point. The latter check point for unknown reason is positioned higher than it appears on screen during componentDidMount, resulting in my visibility check returning true even though it is not visible. If I refresh the page, its position is correct again and the visibility check is fine.
This problem only seem to occur during the first time when I open up a new tab to load my code, but no longer occurs after a refresh on that tab.
Any idea why?