can someone help me debug this issue?
I am at the VueJS course and I have this weird issue (it happens even if I copy-paste source code from the course and it seems like the guy does not have it).
So here is the app image:
When I click on teams in the navigation it opens me all the available teams with some info and the option to see members of each team.
When I click on Team 2 it should go to team 2 and its details.
Here is the working code:
<template>
<section>
<h2>{{ teamName }}</h2>
<ul>
<user-item
v-for="member in members"
:key="member.id"
:name="member.fullName"
:role="member.role"
></user-item>
</ul>
</section>
<router-link to="/teams/t2">Team 2</router-link>
</template>
<script>
import UserItem from '../users/UserItem.vue';
export default {
components: {
UserItem
},
inject: ['users', 'teams'],
data() {
return {
teamName: '',
members: []
};
},
methods: {
loadMembersData(route) {
const teamId = route.params.teamId;
const selectedTeam = this.teams.find((team) => team.id === teamId);
const foundMembers = selectedTeam.members;
const selectedMembers = [];
for (const member of foundMembers) {
const selectedUser = this.users.find((user) => user.id === member);
selectedMembers.push(selectedUser);
}
this.members = selectedMembers;
this.teamName = selectedTeam.name;
}
},
created() {
this.loadMembersData(this.$route);
},
watch: {
$route(newRoute) {
this.loadMembersData(newRoute);
}
}
};
</script>
<style scoped>
section {
margin: 2rem auto;
max-width: 40rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
padding: 1rem;
border-radius: 12px;
}
h2 {
margin: 0.5rem 0;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
</style>
But when I add watcher then my navigation does not work and I get this issue:
Any ideas on how to solve this?
Thanks
Note: I am working with VueJS 3 if it means anything :)
SOLVED:
methods: {
loadMembersData(route) {
const teamId = route.params.teamId;
const selectedTeam = this.teams.find((team) => team.id === teamId);
if (selectedTeam) {
const foundMembers = selectedTeam.members;
const selectedMembers = [];
for (const member of foundMembers) {
const selectedUser = this.users.find((user) => user.id === member);
selectedMembers.push(selectedUser);
}
this.members = selectedMembers;
this.teamName = selectedTeam.name;
}
}
},
Solution that solved the issue:
methods: {
loadMembersData(route) {
const teamId = route.params.teamId;
const selectedTeam = this.teams.find((team) => team.id === teamId);
if (selectedTeam) {
const foundMembers = selectedTeam.members;
const selectedMembers = [];
for (const member of foundMembers) {
const selectedUser = this.users.find((user) => user.id === member);
selectedMembers.push(selectedUser);
}
this.members = selectedMembers;
this.teamName = selectedTeam.name;
}
}
},
Related
I have the following code:
HTML:
<div id="root">
<form class="todoForm">
<input class="input" placeholder="What's gotta be done'?" />
</form>
<div class="todos"></div>
<div class="footer">
<button class="All">All</button>
<button class="Active">Active</button>
<button class="Completed">Completed</button>
<button class="Clear">Clear</button>
</div>
</div>
CSS:
.todoForm {
margin-bottom: 20px;
}
.todos {
margin-bottom: 20px;
}
.todo {
margin-bottom: 10px;
}
.todoAndCheckBox {
display: flex;
font-weight: bold;
}
.checkBox {
border-radius: 50%;
outline: green;
}
.crossOut {
font-weight: normal;
text-decoration: line-through
}
JS:
const COMPLETED = 'completed';
const ACTIVE = 'active';
const ALL = 'all';
class displayTodos {
constructor(root) {
this.root = root;
this.input = this.root.querySelector('.input');
this.form = this.root.querySelector('.todoForm');
this.form.addEventListener('keydown', this.submitForm);
this.todos = this.root.querySelector('.todos');
this.store = {
todos: [
{
id: Math.random() * 10000,
text: 'Banana',
state: COMPLETED,
},
{
id: Math.random() * 10000,
text: 'Ice cream',
state: ACTIVE
}
],
}
this.AllButton = this.root.querySelector('.All');
this.ActiveButton = this.root.querySelector('.Active');
this.CompletedButton = this.root.querySelector('.Completed');
this.display();
}
submitForm = (e) => {
if(e.key === 'Enter') {
e.preventDefault();
const typed = this.input.value;
const newTodo = {
id: Math.random * 10000,
text: typed,
state: ACTIVE
}
const newTodos = [...this.store.todos, newTodo];
this.store.todos = newTodos;
this.display();
this.input.value = ''
}
}
display = () => {
while(this.todos.firstChild) {
this.todos.removeChild(this.todos.firstChild)
}
this.store.todos.forEach(todo => {
const { id, text, state } = todo;
const todoAndCheckBox = document.createElement('div');
const todoDiv = document.createElement('div');
todoAndCheckBox.classList.add('todoAndCheckBox');
todoDiv.innerText = todo.text;
const checkBox = document.createElement('input');
checkBox.setAttribute('type', 'checkbox');
checkBox.classList.add('checkBox');
this.todos.appendChild(todoAndCheckBox);
todoAndCheckBox.appendChild(checkBox);
todoAndCheckBox.appendChild(todoDiv);
todoAndCheckBox.classList.add('todo');
todoAndCheckBox.addEventListener('click', (e, todo) => this.clickHandler(e, todo));
this.displayCount(this.AllButton, ALL);
this.displayCount(this.ActiveButton, ACTIVE);
this.displayCount(this.CompletedButton, COMPLETED);
})
}
clickHandler = (e, todo) => {
e.currentTarget.classList.toggle('crossOut');
console.log(todo, 'todo')
todo.state = todo.state === COMPLETED ? ACTIVE : COMPLETED
}
displayCount = (button, type) => {
let count = 0;
if(type === ALL) {
count = this.store.todos.length;
button.innerText = `All: ${count}`;
}
if(type === ACTIVE) {
const filtered = this.store.todos.filter(todo => todo.state === ACTIVE);
count = filtered.length;
button.innerText = `Active: ${count}`;
}
if(type === COMPLETED) {
const filtered = this.store.todos.filter(todo => todo.state === ACTIVE);
count = filtered.length;
button.innerText = `Completed: ${count}`;
}
}
}
const root = document.querySelector('#root');
const instance = new displayTodos(root);
The desired outcome is when I click to cross out a todo item, the Active and Completed buttons will display the appropriate count number. However, in this function:
clickHandler = (e, todo) => {
e.currentTarget.classList.toggle('crossOut');
console.log(todo, 'todo')
todo.state = todo.state === COMPLETED ? ACTIVE : COMPLETED
}
I'm not able to get out the todo item. When I console.log it out, it says it is undefined. I'm confused because I did pass it down here using:
todoAndCheckBox.addEventListener('click', (e, todo) => this.clickHandler(e, todo));
Why am I not getting the todo and what can I do to get around it?
I noticed that if I rewrite that line to
todoAndCheckBox.addEventListener('click', this.clickHandler.bind(this, e, todo));
JS complains that e is not defined. How do I get e there?
You just need to remove the todo from the anonymous function's parameters in the listener.
By passing the todo as a parameter you are overwriting the todo that is already in scope, and the function is specting an argument that is not being passed to it.
Try this:
todoAndCheckBox.addEventListener('click', (e) => this.clickHandler(e, todo));
You can use an anonymous function in your listener. From there you could call another function or process your logic directly.
const otherFunction = (e) => {
console.log(todo); // global
console.log(e);
}
let todo = {
thing: 'stuff'
};
let todoAndCheckBox = document.getElementById('todoAndCheckBox');
todoAndCheckBox.addEventListener('click', (e) => otherFunction(e));
<button id='todoAndCheckBox'> click </button>
I am working on a site where I am trying to display paginated student information. For example I have 12 students I have pagination items 1-4 as I display 3 students at a time.
I am running into a problem when I call my update pagination function. The counter in this method is always 0 despite me incrementing it correctly. Any ideas or pointers would be greatly appreciated.
import React, {useState, useEffect} from 'react';
function App() {
let students = [
{'name': 'Harry'},
{'name': 'Hermoine'},
{'name': 'Ron'},
{'name': 'Ginny'},
{'name': 'Snape'},
{'name': 'Bellatrix'},
{'name': 'Albus'},
{'name': 'Dudley'},
{'name': 'Petunia'},
{'name': 'Hagrid'},
{'name': 'Lee'},
{'name': 'James'}
];
let [loaded, setLoaded] = useState(false);
let [pagination, setPagination] = useState([]);
let [limit, setLimit] = useState(3); // how many students are visible at a time
let [pages, setPages] = useState(Math.round(students.length/limit)); // amount of pages based on total students
let [currentPage, setCurrentPage] = useState(1); // the current page
let [pageCount, setPageCount] = useState(0); // counter used to increment/decrement pages in view
function updatePagination() {
let tmpArray = [];
for(let i = pageCount; i < pages; i ++){
tmpArray.push(i+1);
}
// 1-3, 4-6 etc...
setPagination(tmpArray.slice(pageCount, pageCount + limit)); // this pageCount is always 0 despite me setting it on handleNext method
}
function handleNext(){
setCurrentPage(currentPage + 1);
if(pageCount + limit === currentPage){
if(currentPage <= pages){
setPageCount(pageCount + limit);
}
}
updatePagination();
}
useEffect(() => {
updatePagination();
setLoaded(true);
}, []);
return (
<main className={styles.app}>
<div className={styles.student}>
<h1>Three student cards</h1>
</div>
<ol className={styles.pagination}>
<div className={styles.paginationContainer}>
<button className={currentPage === 0 ? styles.prev + ' ' + styles.disabled : styles.prev}>prev</button>
{loaded && pagination.map((item, index) => {
return (
<li key={index} className={styles.paginationItem}>
<button className={currentPage === item ? styles.active : null}>{item}</button>
</li>
)
})}
<button onClick={() => {
handleNext()
}} className={currentPage === pages ? styles.next + ' ' + styles.disabled : styles.next}>next</button>
</div>
</ol>
{loaded &&
<div className={styles.info}>
<p>Page Count: {pageCount}</p>
<p>Current Page: {currentPage}</p>
<p>Limit: {limit}</p>
<p>Pages: {pages}</p>
<p>Total Students: {students.length}</p>
<p>Pagination: {pagination}</p>
</div>
}
</main>
);
}
export default App;
Following are the problems in your code:
Inside handleNext() function, you are using currentPage immediately after calling setCurrentPage(...) function BUT the state is updated asynchronously. So currentPage will be the old value instead of the updated value.
Another problem is that if the user clicks the next button three times, then the conditon pageCount + limit === currentPage will be true and pageCount will be set to pageCount + limit which is 3. This means that the loop inside handleNext() function for (let i = pageCount; i < pages; i++) {...} will only execute once. So tmpArray will contain only one element, i.e. 4.
Also, since calling handleNext() function updates the currentPage depending on its previous value, you should update the state by passing the function to setCurrentPage() function
setCurrentPage((page) => page + 1);
Similarly to go to previous page:
setCurrentPage((page) => page - 1);
This will make sure that state is updated correctly.
Edit:
It wasn't clear before why you were using pageCount but the demo you posted in a comment made it clear what you are trying to achieve.
Problem you mentioned in your question is that value of pageCount is always zero. Looking at your code, i think you don't need pageCount at all.
To achieve the desired functionality, you need to take following steps:
To populate the pagination array, you can make use of the useEffect hook that executes whenever students array or limit changes.
useEffect(() => {
// set pagination
let arr = new Array(Math.ceil(students.length / limit))
.fill()
.map((_, idx) => idx + 1);
setPagination(arr);
setLoaded(true);
}, [students, limit]);
To display limited number of students at a time, create a function that slices the students array depending on the currentPage and limit.
const getPaginatedStudents = () => {
const startIndex = currentPage * limit - limit;
const endIndex = startIndex + limit;
return students.slice(startIndex, endIndex);
};
To display a limited number of pages equal to the limit, you don't need pageCount. You can create a function that slices the pagination array depending on the currentPage and limit. This function is similar to the one created in step 2 but the difference lies in how startIndex is calculated.
const getPaginationGroup = () => {
let start = Math.floor((currentPage - 1) / limit) * limit;
let end = start + limit;
return pagination.slice(start, end);
};
Create functions that will change the currentPage
function goToNextPage() {
setCurrentPage((page) => page + 1);
}
function goToPreviousPage() {
setCurrentPage((page) => page - 1);
}
function changePage(event) {
const pageNumber = Number(event.target.textContent);
setCurrentPage(pageNumber);
}
That's all you need to get the desired functionality.
Demo
Following code snippet shows an example:
const studentsData = [
{ name: "Harry" },
{ name: "Hermoine" },
{ name: "Ron" },
{ name: "Ginny" },
{ name: "Snape" },
{ name: "Bellatrix" },
{ name: "Albus" },
{ name: "Dudley" },
{ name: "Petunia" },
{ name: "Hagrid" },
{ name: "Lee" },
{ name: "James" },
{ name: "Lily" },
{ name: "Remus" },
{ name: "Voldemort" },
{ name: "Dobby" },
{ name: "Lucius" },
{ name: "Sirius" }
];
function Student({ name }) {
return (
<div className="student">
<h3>{name}</h3>
</div>
);
}
function App() {
let [students] = React.useState(studentsData);
let [pagination, setPagination] = React.useState([]);
let [loaded, setLoaded] = React.useState(false);
let [limit] = React.useState(3);
let [pages] = React.useState(Math.round(students.length / limit));
let [currentPage, setCurrentPage] = React.useState(1);
function goToNextPage() {
setCurrentPage((page) => page + 1);
}
function goToPreviousPage() {
setCurrentPage((page) => page - 1);
}
function changePage(event) {
const pageNumber = Number(event.target.textContent);
setCurrentPage(pageNumber);
}
React.useEffect(() => {
// set pagination
let arr = new Array(Math.ceil(students.length / limit))
.fill()
.map((_, idx) => idx + 1);
setPagination(arr);
setLoaded(true);
}, [students, limit]);
const getPaginatedStudents = () => {
const startIndex = currentPage * limit - limit;
const endIndex = startIndex + limit;
return students.slice(startIndex, endIndex);
};
const getPaginationGroup = () => {
let start = Math.floor((currentPage - 1) / limit) * limit;
let end = start + limit;
return pagination.slice(start, end);
};
return (
<main>
<h1>Students</h1>
{loaded && (
<div className="studentsContainer">
{getPaginatedStudents().map((s) => (
<Student key={s.name} {...s} />
))}
</div>
)}
<ol className="pagination">
<div className="paginationContainer">
<button
onClick={goToPreviousPage}
className={currentPage === 1 ? "prev disabled" : "prev"}
>
prev
</button>
{loaded &&
getPaginationGroup().map((item, index) => {
return (
<li key={index} className="paginationItem">
<button
onClick={changePage}
className={currentPage === item ? "active" : null}
>
{item}
</button>
</li>
);
})}
<button
onClick={goToNextPage}
className={currentPage === pages ? "next disabled" : "next"}
>
next
</button>
</div>
</ol>
</main>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
h1 { margin: 0; }
.studentsContainer {
display: flex;
background: #efefef;
padding: 15px 10px;
margin: 5px 0;
}
.student {
flex-grow: 1;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
text-align: center;
max-width: 450px;
margin: 0 5px;
}
.pagination {
position: relative;
padding: 0;
}
.paginationContainer {
align-items: center;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(40px, auto));
grid-column-gap: .65rem;
justify-content: center;
justify-items: center;
max-width: 100%;
width: 100%;
position: relative;
}
.paginationItem {
height: 40px;
width: 40px;
user-select: none;
list-style: none;
}
.prev, .next {
user-select: none;
cursor: pointer;
background: transparent;
border: none;
outline: none;
}
.prev.disabled, .next.disabled {
pointer-events: none;
opacity: .5;
}
button {
padding: .65rem;
display: block;
height: 100%;
width: 100%;
border-radius: 50%;
text-align: center;
border: 1px solid #ccc;
outline: none;
cursor: pointer;
}
button.active {
background: rgba(black, .25);
border: none;
color: #777;
pointer-events: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can also view this demo on codesandbox.
So I am trying to add elements taken from an input box, put that into an array, and then display that in an unordered list. My problem is adding a <br> after each entry from Javascript after the value has been passed to the list.
Here is the code used to push to the list currently.
function addToList(){
var incompleteList = document.getElementById("incompleteTaskList");
incompleteList.append(currentTask);
}
Not sure how I would add a break after each new entry
You should be creating new <LI> elements and setting their text content to the task name.
const btnAdd = document.querySelector('.btn-add')
const addTask = e => {
let txtTask = document.querySelector('.txt-task'),
task = txtTask.value.trim()
if (task) {
let li = document.createElement('li')
li.textContent = task
document.querySelector('.tasks').append(li)
txtTask.value = ''
}
}
btnAdd.addEventListener('click', addTask)
h1 { font-size: 1.25rem; }
h2, label { font-size: 1.00rem; }
<h1>Task Manager</h1>
<div>
<label>New Task</label>
<input class="txt-task" type="text" />
<button class="btn-add">Add</button>
</div>
<h2>Tasks</h2>
<ul class="tasks"></ul>
Rendering application
Here is an example using a list data structure and rendering engine. It features auto-sorting and duplicate entry detection.
const main = () => {
new TaskManager('.task-app', {
formConfig: {
fieldLabel: 'New Task',
buttonText: 'Add'
},
listConfig: {
autoSort: true
}
})
}
class TaskForm {
constructor(options) {
let opts = Object.assign({}, TaskForm.defaultOptions, options)
this.fieldLabel = opts.fieldLabel
this.buttonText = opts.buttonText
}
addEvents(target) {
target.querySelector('.btn-add').addEventListener('click', e => {
let txtTask = document.querySelector('.txt-task'),
task = txtTask.value.trim()
if (task) {
const addEvent = new CustomEvent('task-add', {
detail: txtTask.value
})
target.dispatchEvent(addEvent);
txtTask.value = ''
}
})
}
render() {
return `
<div>
<label>${this.fieldLabel}</label>
<input class="txt-task" type="text" />
<button class="btn-add">${this.buttonText}</button>
</div>
`
}
}
TaskForm.defaultOptions = {
fieldLabel: 'New Task',
buttonText: 'Add'
}
class TaskList {
constructor(options) {
this.tasks = []
let opts = Object.assign({}, TaskList.defaultOptions, options)
this.autoSort = opts.autoSort
}
addTask(task) {
if (!this.tasks.includes(task)) {
this.tasks.push(task)
if (this.autoSort) this.tasks.sort()
} else {
console.log(`Task "${task}" already exists.`)
}
}
addEvents(target) {
target.addEventListener('task-add', e => {
this.addTask(e.detail)
const addEvent = new CustomEvent('task-added')
target.dispatchEvent(addEvent);
e.stopImmediatePropagation()
})
}
render() {
return `
<ul>
${this.tasks.map(task => `<li>${task}</li>`).join('')}
</ul>
`
}
}
TaskList.defaultOptions = {
autoSort: false
}
class Mountable {
constructor(target, options) {
this.target = typeof target === 'string' ?
document.querySelector(target) : target
this.initialize(options)
this.update()
}
update() {
this.target.innerHTML = this.render()
this.afterRender()
}
}
class TaskManager extends Mountable {
constructor(target, options) {
super(target, options)
}
initialize(options) {
this.form = new TaskForm(options.formConfig)
this.taskList = new TaskList(options.listConfig)
}
afterRender() {
this.target.addEventListener('task-added', e => {
this.update()
e.stopImmediatePropagation()
})
this.form.addEvents(this.target)
this.taskList.addEvents(this.target)
}
render() {
return `
<h1>Task Manager</h1>
${this.form.render()}
<h2>Tasks</h2>
${this.taskList.render()}
`
}
}
main()
h1 { font-size: 1.25rem; }
h2, label { font-size: 1.00rem; }
<div class="task-app"></div>
You should not be adding a <br> using javascript. That's not the correct implementation approach.
Instead, you need to style this using CSS. Try adding below style rule:
#incompleteTaskList li{display:block;}
i want to show all available usernames when user types # in input field and filtered usernames when user enters anything after # character.
I have implemented like below,
class UserMention extends React.purecomponent {
constructor(props) {
super(props);
this.state = {
text: '',
user_mention: false,
};
this.user='';
}
user_list = [
{name: 'John smith'},
{name: 'Jenna surname2'},
{name: 'Tuija rajala'},
];
get_user = s => s.includes('#') && s.substr(s.lastIndexOf('#') +
1).split(' ')[0];
handle_input_change = (event) => {
let user_mention;
this.user = this.get_user(event.target.value);
if (event.target.value.endsWith('#')) {
user_mention = true;
} else {
user_mention = false;
}
this.setState({
user_mention: user_mention,
[event.target.name]: event.target.value,
});
};
get_text_with_user_mention = (text, selected_user) => {
let user_name = selected_user;
let text_without_user_mention;
text_without_user_mention = text.slice(0,
text.lastIndexOf('#'));
return text_without_user_mention + user_name;
};
handle_select_value = (selected_user) => {
let text;
text = this.get_text_with_user_mention(this.state.text,
selected_user);
this.setState({
text: text,
user_mention: false,
});
this.user = false;
};
render = () => {
let suggested_values = [];
if (this.state.user_mention) {
suggested_values = this.user_list
.map((o) => { return {user_name: o.user_name};});
}
if (this.user) {
suggested_values = this.user_list
.filter(user => user.user_name.indexOf(this.user) !==
-1)
.map((o) => {return {user_name: o.user_name};});
}
return (
<input
required
name="text"
value={this.state.text}
onChange={this.handle_input_change}
type="text"/>
{this.state.user_mention &&
<SelectInput
on_change={this.handle_select_value}
values={suggested_values}/>}
{this.user &&
<SelectInput
on_change={this.handle_select_value}
values={suggested_values}/>}
);
};
}
As you see from above code, i am modifying suggested_values based on this.user and this.state.user_mention state. Can someone help me refactor or modify this a bit more nicer. thanks.
This is another approach using React hooks, instead of classes. If you've never worked with hooks, give it a try. You will enjoy it. It's much simpler in my opinion.
I also added a username property. It's much better if you work with a string that doesn't allow spaces when you're tagging someone. You can also display the full name with spaces along with the username, if you wish.
Ex:
John Smith (#johnsmith)
function App() {
const inputRef = React.useRef(null);
const [inputValue, setInputValue] = React.useState('');
const [userList,setUserList] = React.useState([
{name: 'John smith', username:'johnsmith'},
{name: 'Jenna surname2', username:'jennasurname2'},
{name: 'Tuija rajala', username:'tuijarajala'}
]
);
const [showSuggestions,setShowSuggestions] = React.useState(false);
const [suggestionList,setSuggestionList] = React.useState(
['johnsmith','jennasurname2','tuijarajala']
);
function onChange(event) {
const regexp = /#[a-zA-Z0-9]*$/;
if (regexp.test(event.target.value)) {
setShowSuggestions(true);
}
else {
setShowSuggestions(false);
}
setInputValue(event.target.value);
}
function focusInput() {
inputRef.current.focus();
}
return(
<React.Fragment>
<input ref={inputRef} type='text' value={inputValue} onChange={onChange}/>
{showSuggestions &&
<Suggestions
inputValue={inputValue}
suggestionList={suggestionList}
applyMention={onChange}
focusInput={focusInput}
/>
}
</React.Fragment>
);
}
function Suggestions(props) {
function selectSuggestion(username) {
const regexp = /#[a-zA-Z0-9]*$/;
const newValue = props.inputValue.replace(regexp,username + ' ');
props.applyMention({target: {value: newValue}}); // THIS MIMICS AN ONCHANGE EVENT
props.focusInput();
}
const suggestionItems = props.suggestionList.map((item) =>
<div className="item" onClick={()=>selectSuggestion('#' + item)}>#{item}</div>
);
return(
<div className="container">
{suggestionItems}
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
.container {
border: 1px solid silver;
width: 150px;
}
.item {
cursor: pointer;
}
.item:hover {
color: blue;
}
input {
width: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
You can simplify your code by doing something like this.
See sandbox: https://codesandbox.io/s/react-example-kgm2h
import ReactDOM from "react-dom";
import React from "react";
class UserMention extends React.Component {
constructor(props) {
super(props);
this.state = {
text: "",
user_list: [
{ name: "John smith" },
{ name: "Jenna surname2" },
{ name: "Tuija rajala" }
],
suggestions: []
};
}
handleOnChange = e => {
const { value } = e.target;
const { user_list } = this.state;
//show all user suggestions
if (value.includes("#") && value.indexOf("#") === value.length - 1) {
this.setState({
text: value,
suggestions: [...this.state.user_list]
});
//show matching user suggesstions
} else if (value.includes("#") && value.length > 1) {
const stringAfterAt = value.slice(value.indexOf("#") + 1).toLowerCase();
const newSuggestions = user_list.filter(user => {
return user.name.toLowerCase().includes(stringAfterAt);
});
this.setState({
text: value,
suggestions: newSuggestions
});
//display no users if they do not use the # symbol
} else {
this.setState({
text: value,
suggestions: []
});
}
};
createSuggestionsList = () => {
const { suggestions } = this.state;
return suggestions.map(user => {
return <div>{user.name}</div>;
});
};
render = () => {
return (
<div>
<input
required
name="text"
value={this.state.text}
onChange={this.handleOnChange}
type="text"
/>
{this.createSuggestionsList()}
{/* <SelectInput value={this.state.suggestions}/> */}
</div>
);
};
}
ReactDOM.render(<UserMention />, document.getElementById("root"));
I'm not entirely sure how you want to render the suggested users, but you can always just pass down this.state.suggestions as a prop to the SelectInput component.
Main takeaway is to use an additional array in our state for suggestions and update it as the user types into the input. We call {this.createSuggestionsList()} inside render to dynamically create the markup for each suggested user. Or as mentioned above, just pass down the suggestions as a prop.
EDIT: this problem happens in Firefox (Ubuntu 16), but using Chrome I don't have the problem.
Using React.js, and react-select, I have the following situation:
When I click on the select input, the dropdown with options shows, but it closes almost immediately.
Desired behavior: keep it open until I select an option.
Does anyone know why this is happening?
Here is my code (some of it at least).
component containing the select input:
import React from "react";
import { sec } from "../style/Colors";
import Select from "react-select";
const TagSelectForm = ({ onTagSelectChange, options }) => {
return (
<div className="tagselect-main-container">
<Select isMulti={true} onChange={onTagSelectChange} options={options} />
</div>
);
};
export default TagSelectForm;
Parent component:
import React, { Component } from "react";
import ContentRequest from "../components/ContentRequest";
import axios from "axios";
import TagSelectForm from "../components/TagSelectForm";
import styled from "styled-components";
class OverviewPage extends Component {
state = {
contentRequests: [],
contentRequestTags: [],
filterTags: []
};
async componentDidMount() {
const { data: JSON_string } = await axios.get(
"http://localhost:8000/request"
);
const { requests, tags } = JSON.parse(JSON_string);
this.setState({ contentRequests: requests, contentRequestTags: tags });
}
filterByTags = () => {
const { contentRequests } = this.state;
const filteredRequests = contentRequests.filter(request =>
this.testContainsAFilterTag(request.tags)
);
return filteredRequests;
};
handleAddFilterTag = tag => {
const filterTags = [...this.state.filterTags, tag];
this.setState({ filterTags });
};
handleTagSelectChange = selectedTagsList => {
this.setState({ filterTags: selectedTagsList });
};
handleRemoveFilterTag = tagID => {
const filterTags = this.state.filterTags.filter(tag => tag.id !== tagID);
console.log("filterTags", filterTags);
this.setState({ filterTags });
};
setOverViewpageState = (stateName, stateValue) => {
this.setState({ [stateName]: stateValue });
};
testContainsAFilterTag = tags => {
const { filterTags } = this.state;
const filterTagIDs = filterTags.map(tag => tag.value);
return tags.some(tag => filterTagIDs.includes(tag.id));
};
renderRequests = () => {
let { contentRequests } = this.state;
const { filterTags } = this.state;
const { loginToken, userID } = this.props;
if (filterTags.length > 0) {
contentRequests = this.filterByTags();
}
return (
<RequestList>
{contentRequests.map(request => (
<ContentRequest
contentRequests={contentRequests}
key={request.id}
loginToken={loginToken}
request={request}
setOverViewpageState={this.setOverViewpageState}
userID={userID}
/>
))}
</RequestList>
);
};
render() {
const { contentRequestTags, filterTags } = this.state;
return (
<MainContainer>
<PageTitle>Content Request Overview</PageTitle>
<TagSelectForm
onTagSelectChange={this.handleTagSelectChange}
options={contentRequestTags}
/>
{this.renderRequests()}
</MainContainer>
);
}
}
export default OverviewPage;
const MainContainer = styled.div`
box-sizing: border-box;
display; flex;
flex-direction: column;
max-width: 768px;
margin: 0 auto;
padding: 0.5rem;
`;
const PageTitle = styled.h1`
font-size: 1.25rem;
`;
const RequestList = styled.ul`
list-style-type: none;
padding: 0;
`;
I have resolved this issue by wrapping react-select in "label" tag, so my code looks like:
<label>
<Select
name="name"
options={optionsArray}
...
/>
</label>