How to create subList into List structure. React JS - reactjs

I have been given the following task:
There an inpunt field with ADD button. You can create some text messages and post them press ADD. After you create the first one ADD SUBLIST and REMOVE buttons occure. When you press ADD SUBLIST you got a sublist on current list section and this sublist starts with input field and ADD button and able to create text messages and sublists as noted above. After sublist created the ADD SUBLIST BUTTON change for REMOVE SUBLIST.
Below my structure of listReducer.js:
const defaultState = {
elements: [{
id: 0,
text: '',
topPos: false,
downPos: false,
isInputField: true,
isDelButton: false,
isAddButton: true,
isSublist: false,
subList: []
}]
}
export const ADD_VALUE = 'ADD_VALUE'
export const REMOVE_VALUE = 'REMOVE_VALUE'
export const SET_UP = 'SET_UP'
export const SET_DOWN = 'SET_DOWN'
export const ADD_SUBLIST = 'ADD_SUBLIST'
export const listReducer = (state = defaultState, action) => {
switch (action.type) {
case ADD_VALUE:
return {
...state,
elements: [...state.elements, {
...state.elements.map(
(elem) => ({
...elem,
id: Date.now(),
text: action.payload,
isDelButton: true,
isAddButton: false,
isInputField: false
}))[0]
}]
}
case REMOVE_VALUE:
return {...state, elements: state.elements.filter(element => element.id !== action.payload)}
case ADD_SUBLIST:
return {
...state,
elements: [...state.elements, {
...state.elements.map(
(elem) => ({
...elem,
id: Date.now(),
isSubList: true,
subList: [defaultState] //have problem here
}))[0]
}]
}
default:
return state
}
}
export const addValueCreator = (payload) => ({type: ADD_VALUE, payload})
export const removeValueCreator = (payload) => ({type: REMOVE_VALUE, payload})
export const addSubListCreator = (payload) => ({type: ADD_SUBLIST, payload})
Item file
import React from 'react';
import MyInput from "../UI/MyInput";
import MyButton from "../UI/MyButton";
import MyLi from "../UI/MyLi";
import {useState} from "react";
import {useDispatch} from "react-redux";
import {addSubListCreator, addValueCreator, removeValueCreator} from "../reducer/listReducer";
function Item({element, children, ...props}) {
const [value, setValue] = useState('');
const dispatch = useDispatch();
const addElement = (value) => {
dispatch(addValueCreator(value));
};
const removeElement = (id) => dispatch(removeValueCreator(id))
const addSublist = () => {
const newSubElement = {id: Date.now()};
// const currentID = element.id;
dispatch(addSubListCreator(newSubElement))
}
//console.log('Item', element);
return (
<MyLi>
{element.isInputField ? <MyInput
type="text" value={value}
placeholder='Enter some text here'
onChange={event => setValue(event.target.value)}
/> : children}
<div>
{element.isSublist ? <MyButton>Delete Sublist</MyButton> : !element.isAddButton ? <MyButton
onClick={() => addSublist()}>Add
Sublist</MyButton> : null}
{element.isDelButton ? <MyButton
onClick={() => removeElement(element.id)}
>Remove</MyButton> : null}
{element.isAddButton ? <MyButton
onClick={() => {
addElement(value);
setValue('')
}}
>Add</MyButton> : null}
</div>
</MyLi>
);
}
export default Item;
ListItem file
import React, {useState} from "react";
import Item from "../Components/Item";
import {useSelector} from "react-redux";
const ListItem = () => {
const elements = useSelector((state) => state.elements);
//console.log('List elems ', elements)
return (
<ul>
{elements.map(
element => {
console.log('element subList', element.subList);
return (<Item element={element} key={element.id}>{element.text}
{element.subList.length > 0 ? element.subList.map(
sub => {
console.log('sub', sub.elements);
return (
<ul><Item key={sub.elements.id} element={sub.elements}>{sub.elements.text}</Item>
</ul>)
}).reverse() : null}
</Item>)
}).reverse()}
</ul>
);
};
export default ListItem;
How to create correct structure for this?

Related

How can i test add filters button when clicked it opens search filters popup but before the button appears there is loading spinner?

add filter button is appeared when loading spinner is completed(means there is dispatch trigger action called loadListings which set loading to true).
I'm using react testing library, how can i achieve this by using mockedAxios? or any idea.
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
Search.js
import React, {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useNavigate} from '#reach/router';
import Picture from '#/components/picture';
import Button from '#/components/button';
import Pagination from '#/components/pagination';
import Tag from '#/components/tag';
import Spinner from '#/components/basic-spinner';
import Footer from '#/components/footer/footer';
import {
getAppliedSelectFilters,
getListings,
getListingsTotal,
listingsLoading,
} from '#/selectors/search';
import {showPopup} from '#/redux/modules/app/actions';
import {
saveDraftFilters,
clearDraftFilters,
draftSelectFilter,
clearAppliedFilter,
clearAllAppliedFilters,
loadListings,
} from '#/redux/modules/search/actions';
import {getSelectFilters, getSelectFiltersGroups} from '#/selectors/search';
import SearchFilterPopup from '#/components/search-filter-popup';
const Search = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const appliedFilters = useSelector(getAppliedSelectFilters);
const hits = useSelector(getListings);
const hitsCount = useSelector(getListingsTotal);
const loading = useSelector(listingsLoading);
const hitsPerPage = 10;
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
const showFilters = () => {
dispatch(
showPopup({
content: (
<SearchFilterPopup
saveDraftFilters={saveDraftFilters}
clearDraftFilters={clearDraftFilters}
draftSelectFilter={draftSelectFilter}
loadListings={loadListings}
selectFiltersSelector={getSelectFilters}
selectFiltersGroupsSelector={getSelectFiltersGroups}
/>
),
showClose: false,
}),
);
};
const handleTagClicked = item => {
dispatch(clearAppliedFilter(item));
dispatch(loadListings());
};
// eslint-disable-next-line #typescript-eslint/no-unused-vars
const handleClearAllFilters = () => {
dispatch(clearAllAppliedFilters());
dispatch(loadListings());
};
const handleBusinessSelected = ({identity, urlName}) => () => {
navigate(`/listings/${urlName}/${identity}`);
};
const handlePaginationPageClick = data => {
const indexOfLastHit = (data.selected + 1) * hitsPerPage;
const pages = indexOfLastHit - hitsPerPage;
dispatch(loadListings(pages));
};
return (
<section className='np-flex np-w-full np-filter-container np-overflow-x-hidden np-max-w-1300px np-flex-col np-justify-between np-mx-auto np-min-h-screen'>
<div className='np-flex np-pd np-flex-col np-justify-center'>
{!loading ? (
<div className='np-w-full'>
<div className='np-w-full'>
<h3 className='np-font-primary np-result-places np-mb-3 np-pt-8 np-truncate'>
<span>{hitsCount === 0 ? hitsCount : `${hitsCount}+`}</span>{' '}
{`Place${hitsCount !== 1 ? 's' : ''}`} in Dar es Salaam
</h3>
<div className='np-mb-3 np-flex'>
<Button
variant='link'
className='np-font-semibold np-uppercase'
onClick={showFilters}
>
<span>Add Filters</span>
</Button>
</div>
<div className='np-overflow-x-auto np-flex np-w-full'>
{appliedFilters.map(item => (
<Tag
key={item.name}
item={item}
labelKey='label'
toBe='removed'
onClick={handleTagClicked}
className='np-Tag-width'
/>
))}
{appliedFilters.length ? (
<Button variant='link' onClick={handleClearAllFilters}>
Clear all filters
</Button>
) : null}
</div>
</div>
</div>
) : null}
<div className='np-flex np-flex-wrap np-justify-center'>
{loading ? (
<div className='np-pt-32 np-h-80vh np-w-full np-flex np-justify-center'>
<Spinner size='large' label='loading' color='primary' />
</div>
) : hitsCount > 0 ? (
hits.map(hit => (
<div
key={hit.id}
className='np-search-card'
onClick={handleBusinessSelected(hit)}
>
<Picture
height='2/3'
src={`${
process.env.IMAGE_SERVICE_URL
}/cover:entropy/340x226/${hit.photos.length > 0 &&
hit.photos[hit.photos.length - 1].name}`}
className='np-overflow-hidden np-rounded-sm np-cursor-pointer'
/>
<section className='np-py-2 np-leading-normal'>
<h4 className='np-truncate'>{hit.category}</h4>
<h3 className='np-font-primary np-font-medium np-cursor-pointer np-truncate np-capitalize'>
{hit.name}{' '}
</h3>
<h4 className='np-text np-text-gray np-truncate'>
<span className='icon location'></span> {hit.location}
</h4>
</section>
</div>
))
) : null}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
actions.js
export const clearDraftFilters = () => ({
type: types.CLEAR_DRAFT_FILTERS,
});
export const loadListings = payload => ({
type: types.LOAD_LISTINGS,
payload,
});
export const loadListingStart = () => ({
type: types.LOAD_LISTINGS_START,
});
export const loadListingSuccess = payload => ({
type: types.LOAD_LISTINGS_SUCCESS,
payload,
});
export const loadListingError = error => ({
type: types.LOAD_LISTINGS_ERROR,
payload: error,
error: true,
});
epics.js
export const loadListingsEpic = (action$, state$, {API}) =>
action$.pipe(
filter(action => action.type === types.LOAD_LISTINGS),
switchMap(action => {
const features = getAppliedPlaceFeatureFilters(state$.value).join(',');
const category = getAppliedPlaceCategoryFilters(state$.value).join(',');
const q = get(state$.value, 'search.q', '');
const page = action.payload ? action.payload : null;
const query = omitEmpty({q, category, features, page, limit: 12});
return defer(() => API.getListings(query)).pipe(
switchMap(response => {
const [error, result] = response;
if (error) {
return of(actions.loadListingError(error));
}
return of(actions.loadListingSuccess(result));
}),
startWith(actions.loadListingStart()),
);
}),
);
reducers.js
const initialState = {
selectFilters: [],
suggestions: {docs: [], total: 0, loading: false},
};
export default (state = initialState, action) => {
switch (action.type) {
case types.LOAD_LISTINGS_START:
return {
...state,
listings: {loading: true},
};
case types.LOAD_LISTINGS_SUCCESS:
return {
...state,
listings: {loading: false, ...action.payload},
};
case types.LOAD_LISTINGS_ERROR:
return {
...state,
listings: {loading: false, error: action.payload},
};
default:
return state;
}
};
Search.test.js
import React from 'react';
import {render, screen, fireEvent} from '#/utils/testUtils';
import {waitFor} from '#testing-library/react';
import mockedAxios from 'axios';
import Search from './Search';
import { async } from 'rxjs/internal/scheduler/async';
jest.mock('axios');
describe('Search', () => {
afterEach(() => {
jest.resetAllMocks();
});
test('should render search result filters popup when add filters button is clicked', async () =>{
render(<Search />);
await waitFor(() => fireEvent.click(screen.getByRole('button', {name: 'Add Filters'}))
)
})
});

Why the list doesn't re-render after updating the store?

I started learning mobx and got stuck. Why when I change listItems, List doesn't re-render?
I have store:
export const listStore = () => {
return makeObservable(
{
listItems: [],
addItem(text) {
this.listItems.push(text);
}
},
{
listItems: observable,
addItem: action.bound
}
);
};
Component that adds text from input to store:
const store = listStore();
export const ListForm = observer(() => {
const [value, setValue] = useState();
return (
<>
<input type="text" onChange={e => setValue(e.target.value)} />
<button onClick={() => store.addItem(value)}>Add note</button>
</>
);
});
And I have a list component:
const store = listStore();
export const List = () => {
return (
<React.Fragment>
<ul>
<Observer>
{() => store.listItems.map(item => {
return <li key={item}>{item}</li>;
}
</Observer>
</ul>
<ListForm />
</React.Fragment>
);
};
I don't understand what's wrong. Looks like the list doesn't watch the store changing
codesandbox: https://codesandbox.io/s/ancient-firefly-lkh3e?file=/src/ListForm.jsx
You create 2 different instances of the store, they don't share data between. Just create one singleton instance, like that:
import { makeObservable, observable, action } from 'mobx';
const createListStore = () => {
return makeObservable(
{
listItems: [],
addItem(text) {
this.listItems.push(text);
}
},
{
listItems: observable,
addItem: action.bound
}
);
};
export const store = createListStore();
Working example

Checking one checkbox is checking all other checkboxes

I have a checkbox component whose state is handled in Redux Toolkit. When checking one checkbox it's checking all other checkboxes in all rows, but I only want to check the checkbox I click on.
Here's my code below:
Checkbox Component:
export const Checkbox = (props) => {
const dispatch = useDispatch()
const handleCheckBox = () => {
dispatch(checkboxState)
}
const isChecked = useSelector((state) => (
state.isChecked
))
return (
<input type='checkbox' checked={isChecked} onChange={handleCheckBox}/>
)
}
Slice:
const rowState = {
data: [],
isChecked: false,
loading: false
}
export const rowSlice = createSlice({
name: 'rows',
initialState: rowState,
reducers: {
CHECK_ROWS(state) {
state.isChecked = !state.isChecked
},
})
export const checkboxState = rowSlice.actions.CHECK_ROWS()
Then I'm calling the checkbox component in my page:
const handleRows = (rowData) => {
return (
<tr>
<td>
<Checkbox />
</td>
//rest of the code
</tr>
return(
<Table>
{
dataSource.map((data) => (
handleRows(data)
))
}
</Table>
)
This is happening because you are keeping one variable isChecked for the entire component. To make it unique to each data, keep this as an array:
const rowState = {
data: [],
checkedData: [],
loading: false
}
Then, you should update the checkedData array accordingly. Check state will receive an id or an index and remove from checkedData if it is present in checkecData or add to checkedData if it is not present.
An example:
checkedData.includes(index) ? checkedData.filter((d,i) => i !== index) : [...checkedData, index]
Each checkbox should need an index, and the state should keep track of which checkbox that is checked or not.
import { useSelector, Provider, useDispatch } from "react-redux";
import { createAction, createReducer, configureStore } from "#reduxjs/toolkit";
const initialState = { checkboxes: [false, false, false] };
const index = createAction("toggle/index");
const toggleReducer = createReducer(initialState, (builder) => {
builder.addCase(index, (state, action) => {
state.checkboxes[action.payload] = !state.checkboxes[action.payload];
});
});
const store = configureStore({ reducer: toggleReducer });
const toggleIndex = (index) => {
return {
type: "toggle/index",
payload: index,
};
};
export const Checkbox = ({ index }) => {
const dispatch = useDispatch();
const handleCheckBox = () => {
dispatch(toggleIndex(index));
};
const isChecked = useSelector(({ checkboxes }) => checkboxes[index]);
return (
<input type="checkbox" checked={isChecked} onChange={handleCheckBox} />
);
};
const App = () => {
return (
<Provider store={store}>
<div>
<Checkbox index={0} />
<Checkbox index={1} />
<Checkbox index={2} />
</div>
</Provider>
);
};
export default App;

Deleting an item from FlatList fails with "TypeError: (0, _activities.default) is not a function"

I have a screen that displays a FlatList of custom ActivityItem components to which I am trying to assign an onDelete action:
const TravelScreen = props => {
const myActivities = useSelector(state => state.activities.userActivities);
const dispatch = useDispatch();
return (
<FlatList
data={myActivities}
keyExtractor={item => item.id}
renderItem={itemData => (
<ActivityItem
transportation={itemData.item.transportation}
startTime={itemData.item.startTime}
endTime={itemData.item.endTime}
onEdit={() => {}}
onDelete={() => {
dispatch(removeActivity(itemData.item.id));
}}
/>
)}
/>
);
};
store/actions/activities.js:
export const REMOVE_ACTIVITY = 'REMOVE_ACTIVITY';
export const removeActivity = id => {
return {type: REMOVE_ACTIVITY, id: id};
};
store/reducers/activities.js:
import ACTIVITIES from '../../data/dummy-activity-data';
import REMOVE_ACTIVITY from '../actions/activities';
const initialState = {
allActivities: ACTIVITIES,
userActivities: ACTIVITIES.filter(activity => activity.userId === 'u1')
};
export default (state = initialState, action) => {
switch (action.type) {
case REMOVE_ACTIVITY:
const updatedUserActivities = {...state.items};
delete updatedUserActivities[action.id];
return {
...state,
userActivities: updatedUserActivities
};
}
return state;
};
Can you share how are you importing removeActivity?
Are you importing it like this?
import removeActivity from './actitivties'
If so try importing it like this:
import { removeActivity } from './activities'

Deleting an item from my redux state gives multiple errors

I am having 2 issues:
Initially I can add clients to the empty array through action creators and my reducer. However, whenever I delete the items from the list and try to add new clients to it, it gives me an error: TypeError: Invalid attempt to spread non-iterable instance.
When I said I am deleting the items, what really happens is I create the clients, and then when I click on the delete button next to one of them, all of the clients delete. There is not error in the console, but I just want to delete the specific client with the corresponding id.
Here is my code!
Clients.js
import React, { Component } from 'react'
import AddClient from './AddClient'
import {connect} from 'react-redux'
import {deleteClient} from '../../store/actions/clientActions'
class Clients extends Component {
handleClick = (id) => {
console.log(id)
this.props.deleteClient(id)
}
render() {
const {clientList} = this.props
return (
<div className="container mt-5">
<h2>Here Are Your List of Clients...</h2>
{clientList && clientList.map(client => {
return(
<div key={client.id}>
<div>
Client Name: {client.name} | Client Price: {client.price}
<button onClick={() => {this.handleClick(client.id)}}>Delete</button>
</div>
</div>
)
})}
<AddClient/>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
clientList : state.clients.clientList,
}
}
const mapDispatchToProps = (dispatch) => {
return{
deleteClient : (id) => dispatch(deleteClient(id))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Clients)
Actions:
export const addClient = (client) => {
return(dispatch, getState) => {
dispatch({type: 'ADD CLIENT', client})
}
}
export const deleteClient = (id) => {
return(dispatch, getState) => {
dispatch({type: 'DELETE CLIENT', id})
}
}
Reducer:
const initState = {
clientList: []
}
const clientReducer = (state = initState, action) => {
switch (action.type) {
case 'ADD CLIENT' :
action.client.id = Math.random();
let clientList = [...state.clientList, action.client];
clientList.sort((a, b) => a.name.localeCompare(b.name));
return {
clientList
};
case 'DELETE CLIENT' :
const id = action.id;
clientList = state.clientList.filter(client =>
{return client.id !== id});
return clientList;
default : return state;
}
}
export default clientReducer
Lastly, this is AddClient.js
import React, { Component } from 'react'
import {connect} from 'react-redux'
import {addClient} from '../../store/actions/clientActions'
class AddClient extends Component {
state = {
id: null,
name: null,
price: null,
}
handleChange = (e) => {
this.setState({
[e.target.id] : e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault();
this.props.addClient(this.state);
e.target.reset();
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit} className="mt-5">
<h3>Add a new client:</h3>
<label htmlFor="name">Client Name: </label>
<input type="text" id="name" onChange={this.handleChange}/><br/>
<label htmlFor="price">Client Price: </label>
<input type="text" id="price" onChange={this.handleChange}/> <br/>
<button className="btn btn-primary">Add Client</button>
</form>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
addClient: (client) => dispatch(addClient(client))
}
}
export default connect(null, mapDispatchToProps)(AddClient)
Thank you for all the help, I am fairly new to React and Redux. Let me know if there is any other code you would like to see.
Here's how you can accomplish the delete:
export const deleteClient = (id) => {
const index = find the index of the client you want to delete from the array
return(dispatch, getState) => {
dispatch({type: 'DELETE CLIENT', index})
}
}
case 'DELETE CLIENT' :
return {
...state,
clientList: [
...state.clientList.slice(0, action.index),
...state.clientList.slice(action.index + 1)
]
}
I figured it out, the problem is within my clientReducer.js
This needs to change:
case 'DELETE CLIENT' :
const id = action.id;
clientList = state.clientList.filter(client =>
{return client.id !== id});
return clientList;
to...
case 'DELETE CLIENT' :
const id = action.id;
let newClientList = state.clientList.filter(client => {
return id !== client.id;
})
return {clientList : newClientList};
case 'DELETE CLIENT' :
const id = action.id;
const clientList = state.clientList.filter(client =>
{return client.id !== id});
return {
...state,
clientList
}
You're currently returning just an array, instead of an object. Since that's probably the only thing you have in your redux store right now, it's not breaking (in the ADD action), but you probably want to apply the previous state first, then add your newly filtered clientlist to the state you're returning.

Resources