Passing a function to a button within dangerouslySetInnerHTML doesn't work - javascript

I am trying to add a button tag between a text when a pattern is found in react, this is done inside the dangerouslySetInnerHTML attribute. The onClick only works with alert() and console.log(), it doesn't work when I passed a function to it. What am I missing?
export const detectHashTagPattern = (text) => {
if(!text) return '';
let pattern = /This is a follow-up to your previous request #[0-9]+/gi;
let hashTagPattern = /#[0-9]+/;
text = text.replace(pattern, (res) => {
return res.replace(hashTagPattern, `<button onClick={}>${hashTagPattern.exec(res)}</button>`);
});
return text;
};

Well you could change the algorithm like the following:
function clickHandler() {
console.log("do something");
}
window.clickHandler = clickHandler;
export const detectHashTagPattern = (text) => {
if (!text) return "";
let pattern = /This is a follow-up to your previous request #[0-9]+/gi;
let hashTagPattern = /#[0-9]+/;
text = text.replace(pattern, (res) => {
return res.replace(
hashTagPattern,
// use window.<fct>()
`<button onClick="window.${window.clickHandler.name}()">${hashTagPattern.exec(
res
)}</button>`
);
});
return text;
};
You shouldn't go with this approach, as it might have other issues with the rendering in your application (depends what you're doing in the handler).
A better approach would be like this:
const splitSubject = (text) => {
if (!text) {
return [""];
}
let pattern = /This is a follow-up to your previous request #[0-9]+/gi;
if (!pattern.test(text)) {
return [text];
}
let hashTagPattern = /#[0-9]+/;
let result = text.search(hashTagPattern);
return [text.slice(0, result), text.slice(result), text.slice(result + 1)];
};
const Subject = ({ subject, handler }) => {
const splitted = splitSubject(subject);
let content = null;
if (splitted.length === 1) {
content = <span>{splitted[0]}</span>;
} else {
let [info, cmd, reqReference] = splitted;
content = (
<>
<span>{info}</span>
<button onClick={() => handler?.(reqReference)}>{cmd}</button>
</>
);
}
return <p>{content}</p>;
};
export default function App() {
const requestHandler = (num) => {
console.log(`click on '${num}'`);
};
return (
<div>
<Subject
handler={requestHandler}
subject="This is a follow-up to your previous request #9"
/>
<Subject handler={requestHandler} subject="Not matching subject" />
</div>
);
}

Related

JS Function designed to return elements to React component not returning properly

I am trying to check a set of data to see if a string has a particular prefix or suffix and then return the string broken up into <span> elements. The function I have written correctly matches the parts of teh string with the relevant morphemes in my data set but doesn't return the spans split up.
Currently it returns something like this:
<span></span>
<span>word</span>
<span></span>
Im not sure what i am doing wrong. Here is the code:
export const morphemeCheck2 = (word) => {
return (
<span>
<span>{prefixCheck(word)}</span>
<span>{suffixCheck(word)}</span>
<span>{baseWord(word)}</span>
</span>
)
}
const prefixCheck = (x) => {
api.getPrefixes().then((data) => {
let prefixes = data
for (let p = 0; p < prefixes.length; p++) {
let prefix = prefixes[p].affix_name
if (x.startsWith(prefix)) {
return (
<span className={'suffix ' + prefix} onMouseOver={() => {}}>
{prefix}
</span>
)
}
}
})
}
//needs refactoring
const baseWord = (x) => {
if (prefixCheck(x) && suffixCheck(x)) {
const prefix = prefixCheck(x)
const suffix = suffixCheck(x)
const prefixLength = prefix.props.children.length
const suffixLength = suffix.props.children.length
const tail = x.length - suffixLength
return x.substring(prefixLength, tail)
} else if (prefixCheck(x)) {
const prefix = prefixCheck(x)
const prefixLength = prefix.props.children.length
return x.substring(prefixLength, x.length)
} else if (suffixCheck(x)) {
const suffix = suffixCheck(x)
const suffixLength = suffix.props.children.length
const tail = x.length - suffixLength
return x.substring(0, tail)
} else {
return x
}
}
const suffixCheck = (x) => {
api.getSuffixes().then((data) => {
let suffixes = data
for (let s = 0; s < suffixes.length; s++) {
let suffix = suffixes[s].affix_name
if (x.endsWith(suffix)) {
console.log(suffix)
return (
<span className={'suffix ' + suffix} onMouseOver={() => {}}>
{suffix}
</span>
)
}
}
})
}
Here is where it is called in the React component:
const AnalysisArea = () => {
[...]
let text = "recoded"
return (
<React.Fragment>
<Card className="analysisframe">
<p className="replicated-text" onClick={handleOpen}>
{decoder.morphemeCheck2(text)}
</p>
</Card>
</React.Fragment>
)
}
You need to return something from prefixCheck and suffixCheck. Inside those two functions you just executed async call and did nothing with the result(no return statement inside function). I advise you to rewrite in async/await manner(because of readability) and, of course, to return desired result from those functions. If you need more help(to rewrite those functions) feel free to reach.
UPDATED WITH EXAMPLE OF REFACTORED FUNCTIONS
Key point is to wait for the response in order to use that response of your async call and do something with it(return something based on api call response).
const suffixCheck = async (x) => {
const data = await api.getSuffixes(); // You need to await in order to get data that you want to use in order to return something
let suffixes = data
for (let s = 0; s < suffixes.length; s++) {
let suffix = suffixes[s].affix_name
if (x.endsWith(suffix)) {
console.log(suffix)
return (
<span className={'suffix ' + suffix} onMouseOver={() => {}}>
{suffix}
</span>
)
}
}
return '';
}
const prefixCheck = async (x) => {
const data = await api.getPrefixes();
let prefixes = data
for (let p = 0; p < prefixes.length; p++) {
let prefix = prefixes[p].affix_name
if (x.startsWith(prefix)) {
return (
<span className={'suffix ' + prefix} onMouseOver={() => {}}>
{prefix}
</span>
)
}
}
return '';
}

Marking searched results with JavaScript

I made a search bar that takes everything typed into it and checks if the json file has the name, author or category that matches to it.
How could I make it so that everything typed into the search bar gets highlighted in the part where it displays matched results?
Here is a picture of the search bar for example. I typed in FPS and it found a category with FPS in it. I would want the fps part to be highlighted.
How I made the search bar work:
const search = document.getElementById("search");
const matchList = document.getElementById("match-list");
const searchStates = async searchText => {
const res = await fetch("firebase link");
const states = await res.json();
const listaInformacija2 = Object.values(states)
let matches = listaInformacija2.filter(state => {
const regex = RegExp(`^${searchText}`, "gi");
return state.autor.match(regex) || state.naziv.match(regex) || state.kategorija.match(regex);
});
if(searchText.length === 0) {
matches = []
matchList.innerHTML = "";
}
outputHtml(matches);
};
const outputHtml = matches => {
if(matches.length > 0){
const html = matches.map(match => `
<a href=kurs.html?id=${match.id} id="searchedLink">
<div class="course-col" id="allCourses">
<h4>${match.naziv}</h4>
<p>Kategorija: ${match.kategorija}</p>
<p>Autor: ${match.autor}</p>
</div>
</a>
`).join("");
matchList.innerHTML = html;
}
}
search.addEventListener("input", () => searchStates(search.value));
You can add a specific span element to the highlighted text dynamically, I would also avoid making a Firebase call each search, as it would cost a lot of money if the app scale, here is what I got:
const FIREBASE_URL = '<your firebase url>';
const $search = document.getElementById('search');
const $matchList = document.getElementById('match-list');
const data = {};
function escape(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function search(query) {
if (!query || !data.statesList) return [];
const matches = data.statesList.filter(state => query.test(state.autor) || query.test(state.naziv) || query.test(state.kategorija));
return matches;
}
function render(matches, originalValue, query) {
if (!matches.length) {
$matchList.innerHTML = '';
return;
}
const html = matches
.map(match => {
const replacer = `<span class="search-match">${originalValue}</span>`;
return `
<a href=kurs.html?id=${match.id} class="searchedLink">
<div class="course-col" class="allCourses">
<h4>${match.naziv.replace(query, replacer)}</h4>
<p>Kategorija: ${match.kategorija.replace(query, replacer)}</p>
<p>Autor: ${match.autor.replace(query, replacer)}</p>
</div>
</a>`;
})
.join('');
$matchList.innerHTML = html;
}
function inputHandler(event) {
const { value } = event.target;
let query;
if (!value) query = null;
else query = new RegExp(escape(value), 'i');
const matches = search(query);
render(matches, value, query);
}
async function init() {
const response = await fetch(FIREBASE_URL);
data.states = await response.json();
data.statesList = Object.values(data.states);
$search.addEventListener('input', inputHandler);
}
init().catch(console.error);
And your CSS could look like this:
.search-match {
background-color: #e0d392f0;
}

.forEach validates each element but not the whole input? JS

I'm trying to validate a form input with multiple hashtags. I absolutely have to split the input and convert it to lowerCase.
ex) #sun #sea #summer #salt #sand
When I'm typing a hashtag that fails the validation, bubble message pops up and tells me it's wrong.
But if I type the next hashtag correctly, previous bubble message clears and the whole validation fails (form can be sent).
I'm assuming it has something to do with .forEach – possibly there are better solutions I'm not yet aware of.
I'm new to JS and would appreciate your answers very much.
// conditions for validation
const hashtagInput = document.querySelector('.text__hashtags');
const commentInput = document.querySelector('.text__description');
const testStartWith = (hashtag) => {
if (!hashtag.startsWith('#')) {
return 'hashtag should start with #';
}
return undefined;
};
const testShortValueLength = (hashtag) => {
if (hashtag.length === 1) {
return 'hashtag should have something after #';
}
return undefined;
};
const testValidity = (hashtag) => {
const regex = /^[A-Za-z0-9]+$/;
const isValid = regex.test(hashtag.split('#')[1]);
if (!isValid) {
return 'hashtag can't have spaces, symbols like #, #, $, etc, or punctuation marks';
}
return undefined;
};
const testLongValueLength = (hashtag) => {
if (hashtag.length > 20) {
return 'maximum hashtag length is 20 symbols';
}
return undefined;
};
const testUniqueName = (hashtagArray, index) => {
if (hashtagArray[index - 1] === hashtagArray[index]) {
return 'the same hashtag can't be used twice';
}
return undefined;
};
const testHashtagQuantity = (hashtagArray) => {
if (hashtagArray.length > 5) {
return 'only 5 hashtags for each photo';
}
return undefined;
};
const testCommentLength = (commentInput) => {
if (commentInput.value.length >= 140) {
return 'maximum comment length is 140 symbols';
}
return undefined;
};
const highlightErrorBackground = (element) => {
element.style.backgroundColor = '#FFDBDB';
};
const whitenBackground = (element) => {
element.style.backgroundColor = 'white';
};
Here is the validation at work
const testHashtagInput = () => {
const hashtagArray = hashtagInput.value.toLowerCase().split(' ');
hashtagArray.forEach((hashtag, index) => {
let error = testStartWith(hashtag)
|| testShortValueLength(hashtag)
|| testValidity(hashtag)
|| testLongValueLength(hashtag)
|| testUniqueName(hashtagArray, index)
|| testHashtagQuantity(hashtagArray);
if (error) {
highlightErrorBackground(hashtagInput);
hashtagInput.setCustomValidity(error);
} else {
whitenBackground(hashtagInput);
hashtagInput.setCustomValidity('');
}
hashtagInput.reportValidity();
});
if (hashtagInput.value === '') {
whitenBackground(hashtagInput);
hashtagInput.setCustomValidity('');
}
hashtagInput.reportValidity();
};
const testCommentInput = () => {
let error = testCommentLength(commentInput);
if (error) {
highlightErrorBackground(commentInput);
commentInput.setCustomValidity(error);
} else {
whitenBackground(commentInput);
commentInput.setCustomValidity('');
}
commentInput.reportValidity();
};
hashtagInput.addEventListener('input', testHashtagInput);
Yes your forEach reevaluates the entire validity of the input based on the individual items but only the last one actually remains in the end because it's the last one being evaluated.
You could change your evaluation to an array-reducer function; best fit for your intention is the reducer "some" (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some).
It iterates over the list of items and returns whether any one (or more) of the items fullfills the criterion formulated in your callback function.
I didn't get to test it now, but I guess this should do it:
let error = hashtagArray.some((hashtag, index) => {
return (testStartWith(hashtag)
|| testShortValueLength(hashtag)
|| testValidity(hashtag)
|| testLongValueLength(hashtag)
|| testUniqueName(hashtagArray, index)
|| testHashtagQuantity(hashtagArray));
});
if (error) {
highlightErrorBackground(hashtagInput);
hashtagInput.setCustomValidity(error);
} else {
whitenBackground(hashtagInput);
hashtagInput.setCustomValidity('');
}
hashtagInput.reportValidity();
HTH, cheers

Making "Fill in the Blank" in react

I am quite new to react and I am trying to make a fill in the blank app with react. Basically, I have a passage with a word list. I want to replace all occurrences each word with a blank so that the user can type in the answer. After that, if the user clicks the submit button, it displays the result saying how many they got right.
After doing some research, I found reactStringReplace package which can safely replace strings with react components. This is how I generate the blanks in the passage:
getFillInTheBlank() {
let passage = this.passage;
for (var i = 0; i < this.wordList.length; i++) {
let regexp = new RegExp("\\b(" + this.wordList[i] + ")\\b", "gi");
passage = reactStringReplace(passage, regexp, (match, i) => (
<input type="text"></input>
));
}
return <div>passage</div>
}
However, I can't figure out a way to check each input text with respective words to calculate the score when the submit button is clicked. Can anyone suggest a way of doing this? Thank you in advance.
I made it using Mobx, but it can be easily edited to work without this library.
This is the model, which contains the word to guess and the main events callbacks
word-guess.tsx
import { makeAutoObservable } from "mobx";
export class WordGuess {
private wordToGuess: string;
// Needed to select the next empty char when the component gain focus
private nextEmptyCharIndex = 0;
guessedChars: string[];
focusedCharIndex = -1;
constructor(wordToGuess: string) {
this.wordToGuess = wordToGuess;
// In "guessedChars" all chars except white spaces are replaced with empty strings
this.guessedChars = wordToGuess.split('').map(char => char === ' ' ? char : '');
makeAutoObservable(this);
}
onCharInput = (input: string) => {
this.guessedChars[this.focusedCharIndex] = input;
this.focusedCharIndex += 1;
if(this.nextEmptyCharIndex < this.focusedCharIndex){
this.nextEmptyCharIndex = this.focusedCharIndex;
}
};
onFocus = () => this.focusedCharIndex =
this.nextEmptyCharIndex >= this.wordToGuess.length ? 0 : this.nextEmptyCharIndex;
onFocusLost = () => this.focusedCharIndex = -1;
}
Input Component
guess-input.tsx
interface GuessInputProps {
wordGuess: WordGuess;
}
export const GuessInput = observer((props: GuessInputProps) => {
const { guessedChars, focusedCharIndex, onCharInput, onFocus, onFocusLost } =
props.wordGuess;
const containerRef = useRef(null);
const onClick = useCallback(() => {
const ref: any = containerRef?.current;
ref?.focus();
}, []);
useEffect(() => {
const onKeyDown = (params: KeyboardEvent) => {
const key = params.key;
if (focusedCharIndex >= 0 && key.length === 1 && key.match(/[A-zÀ-ú]/)) {
onCharInput(params.key);
// Clear focus when last character is inserted
if(focusedCharIndex === guessedChars.length - 1) {
const ref: any = containerRef?.current;
ref?.blur();
}
}
};
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, [focusedCharIndex, guessedChars]);
return <div className='guess-input'
onClick={onClick} ref={containerRef}
onFocus={onFocus} onBlur={onFocusLost} tabIndex={-1}>
{guessedChars.map((char, index) =>
<CharToGuess key={index} value={char} focused={index === focusedCharIndex} />)
}
</div>;
});
Component representing each one of the characters
char-to-guess.tsx
import './guess-input.scss';
interface CharToGuessProps {
value: string;
focused: boolean;
}
export const CharToGuess = (props: CharToGuessProps) => {
const { focused, value } = props;
return <span className={`char-to-guess ${focused ? ' focused-char' : ''}`}>
{value || '_'}
</span>;
};
You don't want to create blank strings.
Take a look at this code and see if you understand it.
var answer = document.getElementById('guess-input').name;
var hint = document.getElementById('guess-input').value;
function guessAnswer() {
$("button.guess-submit").click(function(event) {
var guess = $('#guess-input').val();
guess = guess.toLowerCase();
if ( guess == answer) {
$('#correct').show();
$('#wrong').hide();
} else {
$('#wrong').show().fadeOut(1000);
$('#guess-input').val(hint);
}
});
}
function enterSubmit() {
$("#guess-input").keyup(function(event){
if(event.keyCode == 13){
$("#guess-submit").click();
}
});
guessAnswer();
}
enterSubmit();
if ( $('#correct').css('display') == 'block') {
alert('hi');
}
I suggest to send a request to server with the questions and answers and return the results. If you save the points or the answers in the frontend, is possible that the game will be altered.

How to limit Max Length of Draft js

How to limit max characters in draft js?
I can get length of the state like that, but how to stop updating component?
var length = editorState.getCurrentContent().getPlainText('').length;
You should define handleBeforeInput and handlePastedText props. In handler-functions, you check the length of current content + length of pasted text and if it reaches the maximum you should return 'handled' string.
UPD 21.03.2018: Upgraded to the last versions of react/react-dom (16.2.0) and Draft.js (0.10.5).
Working example - https://jsfiddle.net/Ln1hads9/11/
const {Editor, EditorState} = Draft;
const MAX_LENGTH = 10;
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty()
};
}
render() {
return (
<div className="container-root">
<Editor
placeholder="Type away :)"
editorState={this.state.editorState}
handleBeforeInput={this._handleBeforeInput}
handlePastedText={this._handlePastedText}
onChange={this._handleChange}
/>
</div>
);
}
_getLengthOfSelectedText = () => {
const currentSelection = this.state.editorState.getSelection();
const isCollapsed = currentSelection.isCollapsed();
let length = 0;
if (!isCollapsed) {
const currentContent = this.state.editorState.getCurrentContent();
const startKey = currentSelection.getStartKey();
const endKey = currentSelection.getEndKey();
const startBlock = currentContent.getBlockForKey(startKey);
const isStartAndEndBlockAreTheSame = startKey === endKey;
const startBlockTextLength = startBlock.getLength();
const startSelectedTextLength = startBlockTextLength - currentSelection.getStartOffset();
const endSelectedTextLength = currentSelection.getEndOffset();
const keyAfterEnd = currentContent.getKeyAfter(endKey);
console.log(currentSelection)
if (isStartAndEndBlockAreTheSame) {
length += currentSelection.getEndOffset() - currentSelection.getStartOffset();
} else {
let currentKey = startKey;
while (currentKey && currentKey !== keyAfterEnd) {
if (currentKey === startKey) {
length += startSelectedTextLength + 1;
} else if (currentKey === endKey) {
length += endSelectedTextLength;
} else {
length += currentContent.getBlockForKey(currentKey).getLength() + 1;
}
currentKey = currentContent.getKeyAfter(currentKey);
};
}
}
return length;
}
_handleBeforeInput = () => {
const currentContent = this.state.editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length;
const selectedTextLength = this._getLengthOfSelectedText();
if (currentContentLength - selectedTextLength > MAX_LENGTH - 1) {
console.log('you can type max ten characters');
return 'handled';
}
}
_handlePastedText = (pastedText) => {
const currentContent = this.state.editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length;
const selectedTextLength = this._getLengthOfSelectedText();
if (currentContentLength + pastedText.length - selectedTextLength > MAX_LENGTH) {
console.log('you can type max ten characters');
return 'handled';
}
}
_handleChange = (editorState) => {
this.setState({ editorState });
}
}
ReactDOM.render(<Container />, document.getElementById('react-root'))
Mikhail's methods are correct, but the handler return value is not. 'not_handled' is a fall-through case that allows the Editor component to process the input normally. In this case, we want to stop the Editor from processing input.
In older versions of DraftJS, it looks like the presence of a string evaluated to 'true' in the handling code, and so the above code behaved correctly. In later versions of DraftJS, the above fiddle doesn't work - I don't have the reputation to post more that one Fiddle here, but try Mikhail's code with v0.10 of DraftJS to replicate.
To correct this, return 'handled' or true when you don't want the Editor to continue handling the input.
Fiddle with corrected return values
For example,
_handleBeforeInput = () => {
const currentContent = this.state.editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length
if (currentContentLength > MAX_LENGTH - 1) {
console.log('you can type max ten characters');
return 'handled';
}
}
See the DraftJS docs on Cancelable Handlers for more.
This is a bit of an old thread, but thought I would share a solution for anyone else facing the problem of character limit and behaviour while pasting text...
Put together something pretty quickly based on the above code by Mikhail to handle this use case which works for me - although I haven't done any work on trying to optimise it.
Basically the handle pasted text looks like so:
const __handlePastedText = (pastedText: any) => {
const currentContent = editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length;
const selectedTextLength = _getLengthOfSelectedText();
if (currentContentLength + pastedText.length - selectedTextLength > MAX_LENGTH) {
const selection = editorState.getSelection()
const isCollapsed = selection.isCollapsed()
const tempEditorState = !isCollapsed ? _removeSelection() : editorState
_addPastedContent(pastedText, tempEditorState)
return 'handled';
}
return 'not-handled'
}
We have a helper function to handle the deletion of the selection prior to pasting new characters which returns the new editor state:
const _removeSelection = () => {
const selection = editorState.getSelection()
const startKey = selection.getStartKey()
const startOffset = selection.getStartOffset()
const endKey = selection.getEndKey()
const endOffset = selection.getEndOffset()
if (startKey !== endKey || startOffset !== endOffset) {
const newContent = Modifier.removeRange(editorState.getCurrentContent(), selection, 'forward')
const tempEditorState = EditorState.push(
editorState,
newContent,
"remove-range"
)
setEditorState(
tempEditorState
)
return tempEditorState
}
return editorState
}
and finally the function to add the pasted text with a limit:
const _addPastedContent = (input: any, editorState: EditorState) => {
const inputLength = editorState
.getCurrentContent()
.getPlainText().length;
let remainingLength = MAX_LENGTH - inputLength;
const newContent = Modifier.insertText(
editorState.getCurrentContent(),
editorState.getSelection(),
input.slice(0,remainingLength)
);
setEditorState(
EditorState.push(
editorState,
newContent,
"insert-characters"
)
)
}
Link to worked example:
https://codesandbox.io/s/objective-bush-1h9x6
As Mikhail mentioned, you need to handle typing and pasting text. Here are both handlers. Note the paste handler will preserve text that is not outside the limit
function handleBeforeInput(text: string, state: EditorState): DraftHandleValue {
const totalLength = state.getCurrentContent().getPlainText().length + text.length;
return totalLength > MAX_LENGTH ? 'handled' : 'not-handled';
}
function handlePastedText(text: string, _: string, state: EditorState): DraftHandleValue {
const overflowChars = text.length + state.getCurrentContent().getPlainText().length - MAX_LENGTH;
if (overflowChars > 0) {
if (text.length - overflowChars > 0) {
const newContent = Modifier.insertText(
state.getCurrentContent(),
state.getSelection(),
text.substring(0, text.length - overflowChars)
);
setEditorState(EditorState.push(state, newContent, 'insert-characters'));
}
return 'handled';
} else {
return 'not-handled';
}
}
Let's think about this for a second. What is called to make the changes? Your onChange, right? Good. We also know the length. Correct? We attact the "worker" which is the onChange:
const length = editorState.getCurrentContent().getPlainText('').length;
// Your onChange function:
onChange(editorState) {
const MAX_LENGTH = 10;
const length = editorState.getCurrentContent().getPlainText('').length;
if (length <= MAX_LENGTH) {
this.setState({ editorState }) // or this.setState({ editorState: editorState })
}
} else {
console.log(`Sorry, you've exceeded your limit of ${MAX_LENGTH}`)
}
I have not tried this but my 6th sense says it works just fine.

Categories