Hi I'm fairly new to React and currently trying to write a word counter. The idea is that once you type in the text box it will then display a list of all the words and the frequency of how often they're used (This would be displayed in the span tag where it says wordCounts). The issue I'm currently having is it only displays one word with the frequency when I want a list. Moreover I honestly feel like could be achieved in a completely different way but again I'm fairly new to React and learning as I go.
If I need to share any more info or more code, please let me know.
React Code
import { Component } from "react";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
firstValue: "",
numberOfCharacters: "",
withoutWhiteSpace: "",
numberOfWords: "",
linesCount: "",
wordSelectionCount: "",
};
}
firstHandle = (event) => {
var input = event.target.value;
const text = document.getElementById("textarea").value;
const linesCount = text.split("/\r|\r\n|\n/").length;
const numberOfCharacters = input === "" ? 0 : input.split("").length;
const withoutWhiteSpace =
input === "" ? 0 : input.split("").filter((char) => char !== " ").length;
const words =
input === "" ? 0 : input.split(" ").filter((word) => word.trim()).length;
const lines = input === "" ? 1 : input.split(/\n/g).length;
this.setState({
firstValue: input,
numberOfCharacters: numberOfCharacters,
withoutWhiteSpace: withoutWhiteSpace,
numberOfWords: words,
linesCount: lines,
wordSelectionCount: "",
});
};
// This function is responsible for the word counting
wordCounter = (e) => {
e.preventDefault();
var keys = [];
var counts = {};
const input = this.state.firstValue
.replace(/\W/g, " ")
.replace(/[0-9]/g, " ")
.split(" ")
.filter((word) => word.trim());
for (let i = 0; i < input.length; i++) {
var word = input[i];
if (counts[word] === undefined) {
counts[word] = 1;
keys.push(word);
} else {
counts[word] += 1;
keys.push(word);
// console.log(keys);
}
keys.sort();
for (let i = 0; i < keys.length; i++) {
var key = keys[i];
var result = key + " - " + counts[key];
console.log(result);
}
this.setState({
wordSelectionCount: result,
});
}
};
render() {
var numberOfCharacters = this.state.numberOfCharacters;
var withoutWhiteSpace = this.state.withoutWhiteSpace;
var words = this.state.numberOfWords;
var lines = this.state.linesCount;
var wordCounts = this.state.wordSelectionCount;
console.log(wordCounts);
return (
<div className="App">
<header className="App-header">
<form>
<h1>Character Counter</h1>
<p>
Characters <span>{numberOfCharacters}</span> Without White Space{" "}
<span>{withoutWhiteSpace}</span> Words <span>{words}</span> Lines{" "}
<span>{lines}</span>
</p>
<textarea
id="textarea"
type="text"
placeholder="Please type some text..."
value={this.firstValue}
onChange={this.firstHandle}
/>
<h1>Word Counting</h1>
{/* This button calls the wordCounter Method which should display all the Word listings */}
<button className="btn" onClick={this.wordCounter}>
Words Count
</button>
<p>
<span>{wordCounts}</span>
</p>
</form>
</header>
</div>
);
}
}
export default App;
Issue
The issue is you are not iterating over wordSelectionCount to render your data, therefore your latest value will be displayed.
You can iterate over wordSelectionCount to render all of the data. But, I have a suggestion for you
Suggestion
First suggestion will be, use functional components and react hooks.
Second, use the power of the object's [key-value] pair to store the word counts.
I have created a codesandbox example if you want to have a look. You can start adding words to see the word counter.
Solution for the existing code
instead of rendering <span>{wordCounts}</span> directly, you should iterate over it. Such as:
this.state.wordSelectionCount && Object.keys(this.state.wordSelectionCount).map(word => (
<span>{word} - {this.state.wordSelectionCount[word]}</span>
)}
Related
<div id="options-container"></div>
<script>
function createInputElements(num) {
const optionsContainer = document.getElementById("options-container");
for (let i = 0; i < num; i++) {
const input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("placeholder", `Input ${i + 1}`);
optionsContainer.appendChild(input);
}
}
const option = 3;
createInputElements(option);
</script>
This is javascript code to create multiple input fields dynamically based on option numbers and wanted to write in react js
You use the useState to do so:
const[option,setOption]=useState(3);
let output=[];
for (let i=0;i<option;i++){
output.push(<input key={"input_"+i} type="text" placeholder={"Input "+(i + 1)}/>)
}
return (
<div id="options-container">
{output}
</div>
);
New to react and still learning. I have an assignment to create a filter component with three dropsdowns that takes information from a JSON file. The idea is the results in the second dropdown will be filtered once the first dropdown has a selected value. The JSON format is:
"destinations": [
{
"id": 8375,
"name": "Bordeaux",
"country": "France",
"category": "wine"
}, ETC
"seasonCategories": {
"spring": [
"wine",
"wonder",
"forest",
"adventure",
"food"
], ETC
I've created a function that feeds the data into the dropdown component and filters it, but it's not returning as I expect: it's creating only one JSX <option> element with the values of all array items listed inside. I need it to generate a new JSX element with the current value of every item in the array. If I call {el[index]} on the last map function I get the right value, so I'm lost as to why it's not generating each in their own <option> tag. The function I'm using is:
function funcCategories(src, val) {
return Object.keys(src)
.filter((flag) => {
return flag === val;
})
.map((el) => {
let v = [];
for (let i = 0; i < src[el].length; i++) {
v.push(src[el][i]);
}
return v;
})
.map((el) => {
return <option className="Dropdown__option">{el}</option>;
});
}
My Dropdown component:
import React from 'react';
class Dropdown extends React.Component {
constructor(props) {
super(props);
this.value = '';
}
render() {
return (
<div className="Dropdown__wrapper">
<label className="Dropdown__label">{this.props.label}</label>
<select
className="Dropdown__select"
value={this.props.value}
onChange={this.props.handleSelect}
>
<option className="Dropdown__option">{this.props.label}</option>
{this.props.func}
</select>
</div>
);
}
}
export default Dropdown;
For you visual thinkers, this is what I'm seeing in the window:
Dropdown result of my current code
I figured it out! I was pushing the array object in the .filter() method and not the elements of each. Not the cleanest code but it works:
let keys = Object.keys(src);
let j = [];
for (let i = 0; i < keys.length; i++) {
if (keys[i] === val) {
alert(keys[i] + ' === ' + src[keys[i]].length)
for (let h = 0; h < src[keys[i]].length; h++) {
j.push(src[keys[i]][h]);
}
}
}
return j.map((el) => {
return <option className="Dropdown__option">{el}</option>;
});
I'm VERY new to React and not sure how to render this nested array from an external JSON file. remittance is the only array here one that has nested values that I need to access and render. It logs to the console fine, but won't separate on setState. I apologize if this looks terrible.
class App extends Component {
constructor(props){
super(props);
this.state = {
data,
rName: "",
rDescription: "",
}
}
componentDidMount(){
console.log("Mounted!");
this.display();
};
display = () => {
for (var i = 0; i < data.length; i++){
var remittanceList = data[i].Remittance;
// console.log(remittanceList);
for (var x = 0; x < remittanceList.length; x++){
var rName = remittanceList[x].PayorName;
var rDescription = remittanceList[x].Description;
console.log(rName + rDescription);
this.setState({rName, rDescription});
}
}
}
render() {
var { rName, rDescription } = this.state;
return (
<div className="App">
<Title/>
{this.state.data.map((each, index) => (
<PayInfo key={index}
name={each.Payee.Name}
fax={each.Payee.Fax}
pan={each.Payment.PAN}
cvv={each.Payment.CVV}
exp={each.Payment.Exp}
payorName={rName}
description={rDescription}
/>
))}
</div>
);
}
}
export default App;
And the JSON file is something like this. Because the amount of remittances can very, I can't hardcode in an index to look for every time.
[
{
"Payee": {
"Name": "Bob",
"Fax": "5555555555",
},
"Payment": {
"PAN": 123456,
"CVV": 123,
"Exp": "1/2018"
},
"Remittance": [
{
"PayorName": "Me",
"Description": "Hello World.",
},
{
"PayorName": "You",
"Description": "Hey world.",
},
{
"PayorName": "Snoop",
"Description": "Bye world.",
}
]
},
And this is the PayInfo.js file I should've posted initially! Not sure if this changes any of the answer I got before?
import React from "react";
import "./PayInfo.css";
const PayInfo = props => (
<div>
<div id="payee">
<p>Payee</p>
<p>{props.name}</p>
<p>Fax: {props.fax}</p>
</div>
<hr></hr>
<div id="payment">
<p>Payment</p>
<p>PAN: {props.pan}</p>
<p>CVV: {props.cvv}</p>
<p>Exp: {props.exp}</p>
</div>
<hr></hr>
<div id="remittance">
<p><strong>Remittance(s)</strong></p>
<p>Payor: {props.payorName}</p>
<p>Description: {props.description} </p>
</div>
<hr></hr>
<hr></hr>
</div>
);
export default PayInfo;
In your display method, you are setting the state inside for loop. so only the last value being set to the state.
Please try the below code.
display = () => {
let totalRemittanceArray = []
for (var i = 0; i < data.length; i++) {
var remittanceList = data[i].Remittance;
for (var x = 0; x < remittanceList.length; x++) {
var rName = remittanceList[x].PayorName;
var rDescription = remittanceList[x].Description;
console.log(rName + rDescription);
totalRemittanceArray.push({ rName, rDescription })
}
}
this.setState({totalRemittanceArray})
}
What you might want to do is something like this:
https://codesandbox.io/s/p36jlrz7jj
As you can see I extracted another component PayerInfo. Also React works perfect with map within the render method. I hope my example helps!
Currently you call setState in the for loop and thus overwrite the state every time, but I guess you figured that one out already. I use Array.map to loop over elements within JSX a lot. Dunno if one could do this more nicely, but there is always a possibility to improve ^^
A small improvement here: You could loop in a separate function first and then return the complete Array: https://reactjs.org/blog/2017/09/26/react-v16.0.html
I am returning a set of information from Spotify in a React Component and want to interrogate the JSON that is returned and highlight the original search term within the artist name. so for example, if you search 'bus' and one of the artists returned is Kate Bush, then this would be highlighted green in 'Kate BUSh'. At the moment I am calling a function from within render(). However, what I get rendered is:
Kate <span style="color:green">Bus</span>h
How do I get render() to read the HTML as HTML (so that Bus would just be green) rather than rendering as text? Relevant code from the React Component below:
// Called from within render() to wrap a span around a search term embedded in the artist, album or track name
underlineSearch(displayString) {
let searchTerm = this.props.searchTerm;
if (displayString.indexOf(searchTerm) !== -1) {
displayString = displayString.replace(searchTerm, '<span style="color:green">'+searchTerm+'</span>');
}
return displayString;
}
render() {
return (
<div className="Track" id="Track">
<div className="Track-information">
<h3>{this.underlineSearch(this.props.trackName)}</h3>
<p>{this.underlineSearch(this.props.artistName)} | {this.underlineSearch(this.props.albumName)}</p>
</div>
</div>
);
}
Your underlineSearch function needs to return React Elements, but right now it is returning a string. You could use a Fragment to make it work:
// Called from within render() to wrap a span around a search term embedded in the artist, album or track name
underlineSearch(displayString) {
const searchTerm = this.props.searchTerm;
const indexOfSearchTerm = displayString.indexOf(searchTerm);
let node;
if (indexOfSearchTerm === -1) {
node = displayString;
} else {
node = (
<React.Fragment>
{displayString.substr(0, indexOfSearchTerm)}
<span style={{color: 'green'}}>
{displayString.substr(indexOfSearchTerm, searchTerm.length)}
</span>
{displayString.substr(indexOfSearchTerm + searchTerm.length)}
</React.Fragment>
);
}
return node;
}
To make your solution even more reusable you can make underlineSearch and wrapper with your styles for highlighting into 2 separate components. Even more, you can search for multiple occurrences of your searchTerm with regex. Found a similar SO question here. I slightly adapted one of the answers there according to your needs, but all credit goes to this amazing and neat solution for highlighting matches of a string in longer texts. Here is the code:
const Match = ({ children }) => (
<span style={{'color':'green'}}>{children}</span>
);
const HighlightMatches = ({ text, searchTerm }) => {
let keyCount = 0;
let splits = text.split(new RegExp(`\\b${searchTerm}\\b`, 'ig'));
let matches = text.match(new RegExp(`\\b${searchTerm}\\b`, 'ig'));
let result = [];
for (let i = 0; i < splits.length; ++i) {
result.push(splits[i]);
if (i < splits.length - 1) {
result.push(<Match key={++keyCount}>{matches[i]}</Match>);
}
}
return (
<p>{result}</p>
);
};
Then in your main component where you render everything you can do this:
render() {
<div className="Track" id="Track">
<div className="Track-information">
<h3>
<HighlightMatches text={this.props.trackName} searchTerm={this.props.searchTerm}/>
</h3>
<p>
<HighlightMatches text={this.props.artistName} searchTerm={this.props.searchTerm} /> |
<HighlightMatches text={this.props.albumName} searchTerm={this.props.searchTerm} />
</div>
</div>
}
To me this seems like the most react-like approach to solve the problem :)
While you can use dangerouslySetInnerHTML (), as the name suggests it is extremely dangerous, since it is prone to XSS attacks, for example:
{artist: "Kate Bush<script> giveMeAllYourCookies()</script>"}
You can split the displayString into an array and render it.
Please note that that my implementation of underlineSearch is buggy, and will not work if there are more than one match.
class Main extends React.Component {
underlineSearch(displayString) {
let searchTerm = this.props.searchTerm;
var index = 0;
var results = [];
var offset = 0;
while(true) {
const index = displayString.indexOf(searchTerm, offset);
if(index < 0) {
results.push(<span>{displayString.substr(offset)}</span>);
break;
}
results.push(<span> {displayString.substr(offset, index)}</span>);
results.push(<strong style={{color: 'green'}}> {displayString.substr(index, searchTerm.length)}</strong>);
offset = index + searchTerm.length;
}
return results;
}
render() {
return <div>
<h3>{this.underlineSearch(this.props.trackName)}</h3>
<p>{this.underlineSearch(this.props.artistName)} | {this.underlineSearch(this.props.albumName)}</p>
</div>
}
}
ReactDOM.render(<Main
trackName="Magic Buses"
artistName="Kate Bush"
albumName="Kate Bush Alubm"
searchTerm="Bus"
/>, document.getElementById('main'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='main'></div>
In my Vue.js application I want to post form data to my Node.js/MongoDB Backend.
This is my source code: https://github.com/markusdanek/t2w-vue/blob/master/src/components/backend/JobEdit.vue
JSON for my job entry: http://t2w-api.herokuapp.com/jobs/591c09a55ba85d0400e5eb61
Relevant code for my question:
HTML:
<div class="row">
<input type='text'
:name="'qual'+index"
v-model="qualifications[index]">
<button #click.prevent="removeQualifiaction(index)">X</button>
</div>
Methods:
onChange(value, $event){
if (!this.job.xmlOnline)
this.job.xmlOnline = []
const index = this.job.xmlOnline.findIndex(v => v == value)
const checked = $event.target.checked
if (checked && index < 0)
this.job.xmlOnline.push(value)
if (!checked && index >= 0)
this.job.xmlOnline.splice(index, 1)
}
removeQualifiaction() {
this.qualifications.splice(this.qualifications.index, 1);
}
Sending the form data with submit button on form end:
editJob() {
let job = Object.assign({}, this.job);
job.qualifications = this.qualifications;
job.responsibility = this.responsibility;
this.$http.post('https://t2w-api.herokuapp.com/jobs/' + this.$route.params.id, job).then(response => {
console.log(response);
}, response => {
console.log(response);
});
}
My problems now:
When I edit a "Job", I have a list of "qualification items", that are input fields in my form.
Clicking the "delete" button results that the first input gets deleted, not the one I am clicking. Done with #thanksd answer.
How do I add a button and method to add a new input field and to append it to my job.qualifications?
In my JobAdd.vue implemented, to add a new entry to job.qualifications, like this:
<a #click.prevent="addQualification">+</a>
addQualification() {
this.qualification.push({ text: '' });
}
addJob() {
let job = Object.assign({}, this.job);
job.qualifications = this.qualification.map(q => q.text);
this.$http.post('https://t2w-api.herokuapp.com/jobs/', job).then(response => {....
Full source for my JobAdd.vue: https://github.com/markusdanek/t2w-vue/blob/master/src/components/backend/JobAdd.vue
this.qualification.push({ text: '' }); doesnt work obviously not in my JobEdit.vue when there are already strings in my job.qualifications.
Change your removeQualifiaction method to use the index being passed in:
removeQualifiaction(index) {
this.qualifications.splice(index, 1);
}