Making "Fill in the Blank" in react - javascript

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.

Related

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

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>
);
}

.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

How to add comma to alphabetic in JavaScript

I am trying to add a comma when user type number or alphabetic. Actually, I applied regex for it and it working for number. When user type number it add comma to every input, but when I type alphabetic it does not work. Could someone please help me how to accept regex to accept both number and alphabetic.
Thanks
Regex
value.replace(/\B(?=(\d{1})+(?!\d))/g, ",");
UPDATE
If you want to insert a comma, every time you hit enter, just repair the commas using the following epxression:
/ BEGIN
(?<=\w) POSITIVE LOOKBEHIND - PRECEDED BY WORD_CHAR
\b WORD BOUNDARY
[\s,]* ZERO OR MORE - WHITE_SPACE OR COMMA_CHAR
\b WORD BOUNDARY
/ END
g FLAGS = GLOBAL
const VK_ENTER = 13
const txt = document.querySelector('#sample-text')
const insertComma = (value) => value.replace(/(?<=\w)\b[\s,]*\b/g, ' , ')
const handleEnterKey = (e) => {
var code = e.keyCode ? e.keyCode : e.which
switch (code) {
case VK_ENTER:
txt.value = insertComma(txt.value)
break
}
}
document.addEventListener('keydown', handleEnterKey)
<form autocomplete="off" onsubmit="return false;">
<input type="text" id="sample-text" value="" />
</form>
And now for something completely different…
Note: Original answer
You can listen for text input changes and swap the value by removing the present commas and re-adding them in the correct places.
For this example, if you type: "Hello 12345", the text will eventually become: "Hello 12,345"
const TYPE_AHEAD_DELAY = 0 // No delay
const SAMLE_TEXT = "Hello / 12345 / World"
let state = {
sampleText: ''
}
const main = () => {
state = createState(state) // Wrap the state in a proxy
init()
autoType('sampleText', SAMLE_TEXT, 250)
}
const processValue = (value, model) => {
switch (model) {
case 'sampleText':
return fixCommas(value)
default:
return value
}
}
const fixCommas = (value) => value
.replace(/(\d),(\d)()/g, '$1$2') // Revert commas
.replace(/(\d)(?=(\d{3})+\b)/g, '$1,') // Add commas back
const autoType = (prop, value, timeout) => {
let pos = 0, interval = setInterval(() => {
state[prop] += value[pos++]
if (pos >= value.length) {
clearInterval(interval)
}
}, timeout)
}
/* #BEGIN BOILERPLATE */
const createState = (state) => {
return new Proxy(state, {
set(target, property, value) {
target[property] = processValue(value, property)
render(property)
return true
}
})
}
const init = () => {
Object.keys(state).forEach(property => {
const input = document.querySelector(`[data-model="${property}"]`)
let timeout
input.addEventListener('keyup', (e) => {
if (TYPE_AHEAD_DELAY) {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
listener(e)
}, TYPE_AHEAD_DELAY)
} else {
listener(e)
}
})
})
}
const listener = (event) => {
const {type, value, dataset} = event.target
state[dataset.model] = value
}
const render = (property) => {
document.querySelector(`[data-model="${property}"]`).value = state[property]
}
main()
<input type="text" data-model="sampleText" />
You're matching any digit [0-9] with /d, if you change to any char it will work with any digit/letter.
Regex
"123abc".replace(/\B(?=(.{1})+(?!.))/g, ",");
var value = document.getElementById("original").innerText;
document.getElementById("modified").innerText = value.replace(/\B(?=(.{1})+(?!.))/g, ",");
var element = document.getElementById("inp");
element.addEventListener("keydown", function($event) {
if($event.code === 'Enter'){
$event.preventDefault();
element.value += ",";
}
});
<span id="original">123abc</span>
<span id="modified"></span>
<input type="text" id="inp">

Handle multiple actions in handleBeforeInput for DraftJS

I am in a fix in this situation. If the user presses '1. ', i change the block to ordered block. Following is the code I do to change it:
_handleBeforeInput(str) {
if (str !== '.') {
return false;
}
const { editorState } = this.state;
const selection = editorState.getSelection();
const currentBlock = editorState.getCurrentContent()
.getBlockForKey(selection.getStartKey());
const blockLength = currentBlock.getLength();
if (blockLength === 1 && currentBlock.getText() === '1') {
this.onChange((utilFn.resetBlockType(editorState, 'ordered-list-item')));
return 'handled';
}
return 'not-handled';
}
However, once the user creates an ordered-list-item block, I want to set a limit on the block being created. I tried to use the answer from this question: [How to limit Max Length of Draft js, however I dont know how can i handle multiple handlers in handlebeforeInput.
I tried using switch case etc. but it wasnt helping.
Please help me with this issue if anyone has faced it. Thanks!
I realized my mistake... it is pretty straightforward with using multiple if-else, following is the adjusted code:
_handleBeforeInput(str) {
const { editorState } = this.state;
const selection = editorState.getSelection();
const currentBlock = editorState.getCurrentContent()
.getBlockForKey(selection.getStartKey());
const blockLength = currentBlock.getLength()
const currentContent = this.state.editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length
if (currentContentLength > 10 - 1){
alert('you can type max ten characters');
return 'handled';
}else if (str !== '.') {
return 'not-handled';
}else if(str === '.'){
if (blockLength === 1 && currentBlock.getText() === '1') {
this.onChange((utilFn.resetBlockType(editorState, 'ordered-list-item')));
return 'handled';
}
}
return 'not-handled';
}

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