When I dispatch "REMOVE_TODO" on button click it does what I want it to do, the problem I'm having is that when it executes. It doesn't return the correct current array length.
Now when I click an item, it will dispatch "TOGGLE_TODO" which will change the font color and put a line-through the text.
Now while toggled and I click the "Clear Completed" button, it toggles "REMOVE_TODO" and works fine. It removes the items toggled. The problem I'm having is that The number doesn't reflex the current amount of items left in the list when I click the button once..
However if I click the button once more (or however many more times) the number updates to the correct total
This is my app code
import React, { useState, useReducer } from 'react';
import { Reducer } from './reducers/reducer';
import './App.css';
function App() {
const [{ todos, todoCount }, dispatch] = useReducer(Reducer, {
todos: [],
todoCount: 0,
completedCount: 0
});
const [text, setText] = useState("");
return (
<div className="App">
<header className="App-header">
<div>ToDo List [ <span style={{color: '#61dafb', margin: '0px', padding: '0px'}}>{ todoCount }</span> ]</div>
<div>
{ todos.map((todo, index) => (
<div
key={index}
onClick={() => dispatch(
{ type: "TOGGLE_TODO", index }
)}
style={{
fontFamily: 'Tahoma',
fontSize: '1.5rem',
textDecoration: todo.completed ? 'line-through' : "",
color: todo.completed ? '#61dafb' : 'dimgray',
cursor: 'pointer'
}}
>
{ todo.text }
</div>
))
}
<form
onSubmit={e => {
e.preventDefault();
text.length === 0 ? alert("No Task To Add!") : dispatch({ type: "ADD_TODO", text });
setText("");
}}
>
<input
type="text"
name="input"
value={ text }
onChange={e => setText(e.target.value)}
/><br />
<button>
Add
</button>
</form>
<button onClick={() => {dispatch({ type: "REMOVE_TODO" })}}>
Clear Completed
</button>
</div>
</header>
</div>
);
}
export default App;
and this is my reducer code
export const Reducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
todos: [...state.todos, { text: action.text, completed: false, id: Date.now() }],
todoCount: state.todoCount + 1,
completedCount: 0
};
case 'TOGGLE_TODO':
return {
todos: state.todos.map((todo, index) => index === action.index ? { ...todo, completed: !todo.completed } : todo),
todoCount: state.todoCount,
completedCount: 0
};
case 'REMOVE_TODO':
return {
todos: state.todos.filter(t => !t.completed),
todoCount: state.todos.length
}
default:
return state;
};
};
Does anyone have any idea what I'm doing wrong, or what I'm not doing? Thanks in advance!
Remove "todoCount" from reducer, then derive count using "todos":
<div>
ToDo List [{" "}
<span style={{ color: "#61dafb", margin: "0px", padding: "0px" }}>
{todos.filter((todo) => !todo.completed).length}
</span>{" "}
]
</div>
View in CodeSandbox here
Related
I'm making a todo app and using useState to pass value to the form then submit the todo but for some reasons my todo form is not render and i don't know what is missing in my codes, please help me to check! Thank you so much!
import React, { useState } from "react";
function Todo({ todo, index }) {
console.log("hiiii");
return (
<div>
<p>{todo.text}</p>
</div>
);
}
function todoForm(addTodo) {
const [value, setValue] = useState("");
handleSubmit = (e) => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue("");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="add new todo"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</form>
</div>
);
}
function App() {
const [todos, setTodos] = useState([
{
text: "eat lunch",
isCompleted: false
},
{
text: "do homework",
isCompleted: false
},
{
text: "go to school",
isCompleted: false
}
]);
addTodo = (text) => {
console.log("hey");
const newTodos = [...todos, { text }];
setTodos(newTodos);
};
return (
<div>
<div>
{todos.map((todo, index) => {
return <Todo key={index} index={index} todo={todo} />;
})}
</div>
<div>
<todoForm addTodo={addTodo} />
</div>
</div>
);
}
export default App;
Link sandbox: https://codesandbox.io/s/serverless-bash-ef4hk?file=/src/App.js
JSX tags must be uppercased in order to be properly parsed by the compiler as a React component.
Instead of todoForm, use TodoForm.
Capitalized types indicate that the JSX tag is referring to a React component. These tags get compiled into a direct reference to the named variable, so if you use the JSX expression, Foo must be in scope.
From: https://reactjs.org/docs/jsx-in-depth.html#specifying-the-react-element-type
Also, you need to destructure props inside TodoForm in order to gain access to addTodo:
// Bad
function TodoForm(addTodo) {...}
// Good
function TodoForm({addTodo}) {...}
You should also assign you handlers to consts:
// Bad
addTodo = (text) => {...};
// Good
const addTodo = (text) => {...};
your problem is solved it
APP.JS
import React, { useState } from "react";
function Todo({ todo, index }) {
console.log("hiiii");
return (
<div>
<p>{todo.text}</p>
</div>
);
}
function todoForm(addTodo) {
const [value, setValue] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!value) return;
addTodo(value);
setValue("");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="add new todo"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</form>
</div>
);
}
function App() {
const [todos, setTodos] = useState([
{
text: "eat lunch",
isCompleted: false
},
{
text: "do homework",
isCompleted: false
},
{
text: "go to school",
isCompleted: false
}
]);
const addTodo = (text) => {
console.log("hey");
const newTodos = [...todos, { text }];
setTodos(newTodos);
};
return (
<div>
<div>
{todos.map((todo, index) => {
return <Todo key={index} index={index} todo={todo} />;
})}
</div>
<div>
{todoForm(addTodo)}
</div>
</div>
);
}
export default App;
Note: I am using the "react-google-maps" api and this is how my current InfoWindow is set up
{showingInfoWindow && selectedPlace === spot._id && <InfoWindow
className="info-window"
onCloseClick={onInfoWindowClose}
position={{lat: spot.lat, lng: spot.lng}}
>
<div className="iw-container">
<strong className="iw-title">{spot.name}</strong>
<div className="iw-content">
{spot.location}
<div>Added By: {currentUser.displayName === spot.user ? "Me" : spot.user}</div>
<div>{spot.type}</div>
<div>{spot.desc}</div>
<div>{moment(spot.createdAt).format("MMM Do YYYY")}</div>
{/* <img src={`/server/uploads/${spot.createdAt.split('.')[0]+"Z"}.jpg`}> </img> */}
</div>
</div>
</InfoWindow>}
I was wondering how I add an image to the infowindow, I've seen it done with a content prop in other api's, and react-google-maps docs has a prop for updating the content, but I can't find how to set the content on their documentation. Any help is appreciated!
You can directly add an <img> tag as a child of the <infowindow>
Sample code snippet:
import React, { Component } from 'react';
import {
withGoogleMap,
GoogleMap,
Marker,
InfoWindow
} from 'react-google-maps';
class Map extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false
};
}
handleToggleOpen = () => {
this.setState({
isOpen: true
});
};
handleToggleClose = () => {
this.setState({
isOpen: false
});
};
render() {
const GoogleMapExample = withGoogleMap(props => (
<GoogleMap
defaultCenter={{ lat: -33.86882, lng: 151.209296 }}
defaultZoom={13}
>
<Marker
key={this.props.index}
position={{ lat: -33.86882, lng: 151.209296 }}
onClick={() => this.handleToggleOpen()}
>
{this.state.isOpen && (
<InfoWindow
onCloseClick={this.props.handleCloseCall}
>
<img src="https://www.australia.com/content/australia/en/places/sydney-and-surrounds/guide-to-sydney/jcr:content/mainParsys/imagecontainer/imageContainerParsys/imagehighlights_835593945/ImageTile/imageHighlightsSrc.adapt.740.medium.jpg" width="250px" height="250px"/>
</InfoWindow>
)}
</Marker>
</GoogleMap>
));
return (
<div>
<GoogleMapExample
containerElement={<div style={{ height: `500px`, width: '500px' }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
</div>
);
}
}
export default Map;
I figured out the problem: I needed to use a self-closing img tag.
instead of
<img src="..."> </img>
it must be
<img src="..."/>
I am doing the search engine section in VueJS and Laravel, but I have a problem that does not allow me to advance in the other sections. The search engine opens and everything but when I write it only sends the first letter or 2 but not all of them like this in this image:
image of the data you send
the data that I write
After that it shows me the following error in console:
Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location: "/search?q=th"
Now showing my search engine code:
<template>
<div class="form_MCycW">
<form autocomplete="off" #sumbit.prevent>
<label class="visuallyhidden" for="search">Search</label>
<div class="field_2KO5E">
<input id="search" ref="input" v-model.trim="query" name="search" type="text" placeholder="Search for a movie, tv show or person..." #keyup="goToRoute" #blur="unFocus">
<button v-if="showButton" type="button" aria-label="Close" #click="goBack">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"><g fill="none" stroke="#fff" stroke-linecap="round" stroke-miterlimit="10" stroke-width="1.5"><path d="M.75.75l13.5 13.5M14.25.75L.75 14.25"/></g></svg>
</button>
</div>
</form>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
query: this.$route.query.q ? this.$route.query.q : ''
}
},
computed: {
showButton() {
return this.$route.name === 'search';
},
...mapState({
search: state => state.event.fromPage
})
},
mounted() {
this.$refs.input.focus();
},
methods: {
goToRoute() {
if (this.query) {
this.$router.push({
name: 'search',
query: { q: this.query },
});
} else {
this.$router.push({
path: this.fromPage,
});
}
},
goBack() {
this.query = '';
this.$router.push({
path: '/',
});
},
unFocus (e) {
if (this.$route.name !== 'search') {
const target = e.relatedTarget;
if (!target || !target.classList.contains('search-toggle')) {
this.query = '';
this.$store.commit('closeSearch');
}
}
}
}
}
</script>
This is the other section of the search engine:
<template>
<main class="main">
<div class="listing">
<div class="listing__head"><h2 class="listing__title">{{ title }}</h2></div>
<div class="listing__items">
<div class="card" v-for="(item, index) in data.data" :key="index">
<router-link :to="{ name: 'show-serie', params: { id: item.id }}" class="card__link">
<div class="card__img lazyloaded"><img class="lazyload image_183rJ" :src="'/_assets/img/covers/posters/' + item.poster" :alt="item.name"></div>
<h2 class="card__name">{{ item.name }}</h2>
<div class="card__rating">
<div class="card__stars"><div :style="{width: item.rate * 10 + '%'}"></div></div>
<div class="card__vote">{{ item.rate }}</div>
</div>
</router-link>
</div>
</div>
</div>
</main>
</template>
<script>
import { mapState } from 'vuex';
let fromPage = '/';
export default {
name: "search",
metaInfo: {
bodyAttrs: {
class: 'page page-search'
}
},
computed: {
...mapState({
data: state => state.search.data,
loading: state => state.search.loading
}),
query() {
return this.$route.query.q ? this.$route.query.q : '';
},
title() {
return this.query ? `Results For: ${this.query}` : '';
},
},
async asyncData ({ query, error, redirect }) {
try {
if (query.q) {
this.$store.dispatch("GET_SEARCH_LIST", query.q);
} else {
redirect('/');
}
} catch {
error({ message: 'Page not found' });
}
},
mounted () {
this.$store.commit('openSearch');
this.$store.commit('setFromPage', fromPage);
if (this.data.length == 0 || this.data === null) {
this.$store.dispatch("GET_SEARCH_LIST", this.query);
}
setTimeout(() => {
this.showSlideUpAnimation = true;
}, 100);
},
beforeRouteEnter (to, from, next) {
fromPage = from.path;
next();
},
beforeRouteUpdate (to, from, next) {
next();
},
beforeRouteLeave (to, from, next) {
const search = document.getElementById('search');
next();
if (search && search.value.length) {
this.$store.commit('closeSearch');
}
}
};
</script>
In my routes section it is defined as follows:
{
name: 'search',
path: '/search',
component: require('../views/' + themeName + '/control/search/index').default
}
It is supposed to be a real-time search engine. I would appreciate your help in solving this problem...
What you need is a debounce. What it does is that it wait or delay till the user had finished typing before the model get updated or before you send it to the server.
An example of how it works is here
Here is a package for it.
https://github.com/vuejs-tips/v-debounce
The Problem
I have a search component and component which implements the search component. When I type something in the search bar after 1/2 second of not typing (debounce) the server should be hit and the results should be returned.
The solution i am trying to implement comes from this post on Stackoverflow
The code
This leads me to the following code.
I have search.vue
<template>
<label for="search">
<input
id="search"
class="w-full py-2 px-1 border-gray-900 border"
type="text"
name=":searchTitle"
v-model="searchFilter"
:placeholder="searchPlaceholder"
autocomplete="off"
v-on:keydown="filteredDataset"
/>
</label>
</template>
<script>
import {debounce} from 'lodash';
export default {
props: {
searchPlaceholder: {
type: String,
required: false,
default: ''
},
searchName: {
type: String,
required: false,
default: 'search'
}
},
data() {
return {
searchFilter: '',
}
},
methods: {
filteredDataset() {
console.log('event fired');
this.$emit('searchValue', this.searchFilter);
}
},
}
</script>
And product.vue
<template>
<div>
<div class="my-4">
<search
search-placeholder=""
search-name=""
v-on:searchValue="filterValue = $event"
v-model="productsFiltered"
>
</search>
<div class="flex w-full py-1 border px-2 my-2" v-for="product in productsFiltered"> (...)
</div>
</div>
</div>
</div>
</template>
<script>
import {debounce} from 'lodash';
export default {
data() {
return {
products: [],
filterValue: '',
filteredProducts: ''
}
},
computed: {
productsFiltered: {
get(){
console.log('getter called');
return this.filteredProducts;
},
set: _.debounce(function(){
console.log('setter called');
if (this.filterValue.length < 1) {
this.filteredProducts = [];
}
axios.get(`${apiUrl}search/` + this.filterValue)
.then(response => {
this.products = response.data.products;
const filtered = [];
const regOption = new RegExp(this.filterValue, 'ig');
for (const product of this.products) {
if (this.filterValue.length < 1 || product.productname.match(regOption)) {
filtered.push(product);
}
}
this.filteredProducts = filtered;
});
}, 500)
}
},
}
</script>
The result
The result is that the setter in the computed property in product.vue does not get called and no data is fetched from the server. Any ideas on how to solve this?
Your first code block imports debounce but does not use it. It also declares a prop, searchName, that isn't used. These aren't central issues, but clutter makes it harder to figure out what's going on.
Your second code block uses v-model but does not follow the required conventions for getting v-model to work with components:
the component must take a prop named value
the component must emit input events to signal changes to value
You have the component emit searchValue events, and handle them with a v-on that sets a data item. You seem to expect the v-model to call the setter, but as I noted, you haven't hooked it up to do so.
From what's here, you don't even really need to store the input value. You just want to emit it when it changes. Here's a demo:
const searchComponent = {
template: '#search-template',
props: {
searchPlaceholder: {
type: String,
required: false,
default: ''
}
},
methods: {
filteredDataset(searchFilter) {
console.log('event fired');
this.$emit('input', searchFilter);
}
}
};
new Vue({
el: '#app',
data() {
return {
products: [],
filterValue: '',
filteredProducts: ''
}
},
components: {
searchComponent
},
computed: {
productsFiltered: {
get() {
console.log('getter called');
return this.filteredProducts;
},
set: _.debounce(function() {
console.log('setter called');
if (this.filterValue.length < 1) {
this.filteredProducts = [];
}
setTimeout(() => {
console.log("This is the axios call");
this.filteredProducts = ['one','two','three'];
}, 200);
}, 500)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<template id="search-template">
<label for="search">
<input
id="search"
class="w-full py-2 px-1 border-gray-900 border"
type="text"
name=":searchTitle"
:placeholder="searchPlaceholder"
autocomplete="off"
#input="filteredDataset"
/>
</label>
</template>
<div id="app">
<div class="my-4">
<search-component search-placeholder="enter something" v-model="productsFiltered">
</search-component>
<div class="flex w-full py-1 border px-2 my-2" v-for="product in productsFiltered"> (...)
</div>
</div>
</div>
I'm coding a to-do list using React hooks.
Every added item has two dropdown list where the user can decide how urgent the task (urgency value) is and how long the thing to do will take (speed value).
Updating either list will add their value into a 'score' property.
By clicking a "Sort" button I can sort the entries based on the score.
Right now the problem is that if I have more then one to-do item with different urgency and speed value, the score will always be the same for both components.
Can somebody help? Thanks
function ToDo(){
const [ input, setInput ] = React.useState('')
const [ toDo, setToDo ] = React.useState([])
const [ score, setScore ] = React.useState(0)
const [ speed, setSpeed ] = React.useState(0)
const [ urgency, setUrgency ] = React.useState(0)
return(
<div>
<h2>List of things to do</h2>
<input
value={ input }
onChange={ (e) => setInput( e.target.value ) }/>
<button
onClick={ () => {
setToDo( toDo.concat(input))
setInput('')
}}>Add
</button>
<ul>
{ toDo.map(( task, idTask ) => {
return (
<li
key={idTask}
score={ speed + urgency }>
{task}<br/>
<select onChange={(e) => { setSpeed(Number(e.target.value)) }}>
<option value={1}>slow</option>
<option value={2}>medium</option>
<option value={3}>fast</option>
</select><br/>
<select onChange={(e) => { setUrgency(Number(e.target.value)) }}>
<option value={1}>non-urgent</option>
<option value={3}>urgent</option>
</select>
<span
onClick={
(index) => {
const newTodos = [...toDo]
newTodos.splice(index, 1);
setToDo( newTodos)
}}>
[-------]
</span>
</li>
)
})
}
</ul>
<button onClick={
() => {
const sortMe = [...toDo].sort((a, b) => b - a)
setToDo( sortMe )
}}>Sort!</button>
</div>
)
}
ReactDOM.render(<ToDo/>, document.getElementById('app'));
You should implement a different data model to achieve that. You should hold an array of objects for your todos (each todo will be an object) and each object should have an urgency property so you can set that individually.
Something like this:
function App() {
const [todos,setTodos] = React.useState([
{ id: 'todo1', text: 'This is todo1', urgency: 0 },
{ id: 'todo2', text: 'This is todo2', urgency: 1 }
]);
function handleClick(id) {
setTodos((prevState) => {
let aux = Array.from(prevState);
aux = aux.map((todo) => {
if (todo.id === id) {
todo.urgency === 0 ? todo.urgency = 1 : todo.urgency = 0;
}
return todo;
});
return aux;
});
}
const todoItems = todos.map((todo) =>
<li
key={todo.id}
className={todo.urgency === 1 ? 'urgent' : 'normal'}
onClick={()=>handleClick(todo.id)}
>
{todo.text}
{!!todo.urgency && '<--- This is urgent'}
</li>
);
return(
<React.Fragment>
<div>
Click on the todos!
</div>
<ul>
{todoItems}
</ul>
</React.Fragment>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
li {
cursor: pointer;
}
.urgent {
color: red;
}
<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"/>