I have a notification component that looks like this:
When I click the bell button, a notification container pops up, like so:
I have the functionality set up so that when a user clicks or touches outside of the notification container, the box closes. However, I do not want to add events to the DOM directly. I'd much rather take advantage of React's SyntheticEvent wrapper. Does anyone know how I could use SyntheticEvent, instead of the DOM events I currently have?
Here is my code so far:
import React from 'react';
import { PropTypes } from 'prop-types';
import './Notification.scss';
export default class Notification extends React.Component {
constructor(props) {
super(props);
this.setNotificationWrapper = this.setNotificationWrapper.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
this.state = {
visible: false,
hasNotifications: false,
expanded: false,
};
this.toggleNotificationContainer = this.toggleNotificationContainer
.bind(this);
}
componentDidMount() {
document.addEventListener('touchend', this.handleClickOutside);
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('touchend', this.handleClickOutside);
document.removeEventListener('mousedown', this.handleClickOutside);
}
setNotificationWrapper(node) {
this.notificationWrapper = node;
}
handleClickOutside(event) {
if (this.notificationWrapper &&
!this.notificationWrapper.contains(event.target)) {
this.toggleNotificationContainer();
event.preventDefault();
}
}
toggleNotificationContainer() {
this.setState({
visible: !this.state.visible,
hasNotifications: this.state.hasNotifications,
expanded: !this.state.expanded,
});
}
createNotificationItems(option, index) {
this.state.hasNotifications = true;
return (
<li
key={index}
value={index}
>
{option.label}
</li>
);
}
render() {
// set up the details to show in the list.
let notificationOptions = null;
let optionCount = 0;
let toggleDivDetails = null;
let buttonStyleName = '';
let buttonText = null;
if (this.props.notificationButtonIcon) {
buttonStyleName = 'notificationButton';
} else {
buttonText = this.props.notificationButtonText;
buttonStyleName = 'notificationButtonWithText';
}
toggleDivDetails = (
<button
onClick={this.toggleNotificationContainer}
styleName={this.state.expanded ? 'expanded' : buttonStyleName}
disabled={this.state.expanded}
>
{buttonText}
</button>
);
if (this.props.options) {
optionCount = this.props.options.length;
notificationOptions = this.props.options.map((option, index) =>
this.createNotificationItems(option, index));
}
if (optionCount === 0) {
this.state.hasNotifications = false;
}
return (
<div styleName="notificationBar">
{toggleDivDetails}
<span
styleName={
this.state.hasNotifications ? 'notificationCount' : 'hidden'
}
>
{optionCount}
</span>
{
this.state.visible &&
<div
styleName="notificationContainer"
ref={this.setNotificationWrapper}
>
<h3 styleName="notificationHeading">Notifications</h3>
<h4 styleName="notificationSubHeading">
{optionCount} notifications
</h4>
<ul styleName="notificationList">
{notificationOptions}
</ul>
</div>
}
</div>
);
}
}
Notification.propTypes = {
options: PropTypes.arrayOf(PropTypes.object),
notificationButtonIcon: PropTypes.bool.isRequired,
notificationButtonText: PropTypes.string.isRequired,
};
Notification.defaultProps = {
options: [],
NotificationButtonIcon: true,
notificationButtonText: 'Toggle Notifications',
};
Related
This is a spelling game. The user drags the correct letter over the dropzone. On drop, the function checkIfLetterIsCorrect is invoked and runs the checking process. When all dropzones are filled with the correct letter the app moves on to the next word and props and state or in sync.
This all works fine on a hard refresh. However, this component is inside a Semantic UI React modal, and when the modal is closed and reopened, and I set the game back up this.props.currentWordLetters is the correct value, but when checkIfLetterIsCorrect is invoked, the value of this.props.currentWordLetters inside this method is of the value before the modal was closed. I checked with a console.log inside ComponentWillUnmount and the component is indeed unmounting when the modal is closed, so I don't see why the values inside checkIfLetterIsCorrect would be outdated as everything should be rendered like new. If somebody could point me in the right direction it would be a big help. Thank you.
class DropzoneContainer extends React.Component {
constructor() {
super();
this.state = {
correctCounter: 0,
};
}
componentWillUnmount(){
console.log('unMounted')
}
checkIfLetterIsCorrect(e) {
for (let i = 0; i < this.props.currentWordLetters.length; i++) {
console.log(this.props.currentWordLetters, this.state.letters);
if (
this.props.currentWordLetters[i] ===
e.relatedTarget.innerText &&
e.relatedTarget.innerText === e.target.innerText
) {
e.relatedTarget.classList.remove('draggable');
this.props.onCorrectLetter();
this.setState({
correctCounter: this.state.correctCounter + 1,
});
if (this.state.correctCounter === this.props.currentWordLetters.length) {
this.setState({ correctCounter: 0 })
clearInterval(this.props.timer.current);
this.props.onTimerStop();
this.props.onRoundComplete();
return;
}
}
}
}
//...
Here is the entire component:
import React from 'react';
import { connect } from 'react-redux';
import interact from 'interactjs';
import { Grid } from 'semantic-ui-react';
import Dropzone from '../dropzone/dropzone.component'
import {
onCorrectLetter,
onRoundComplete,
onTimerStop,
} from '../../../../actions/phonicsGameActions';
class DropzoneContainer extends React.Component {
constructor() {
super();
this.state = {
correctCounter: 0,
};
}
componentWillUnmount(){
console.log('unMounted')
}
checkIfLetterIsCorrect(e) {
for (let i = 0; i < this.props.currentWordLetters.length; i++) {
console.log(this.props.currentWordLetters, this.state.letters);
if (
this.props.currentWordLetters[i] ===
e.relatedTarget.innerText &&
e.relatedTarget.innerText === e.target.innerText
) {
e.relatedTarget.classList.remove('draggable');
this.props.onCorrectLetter();
this.setState({
correctCounter: this.state.correctCounter + 1,
});
if (this.state.correctCounter === this.props.currentWordLetters.length) {
this.setState({ correctCounter: 0 })
clearInterval(this.props.timer.current);
this.props.onTimerStop();
this.props.onRoundComplete();
return;
}
}
}
}
componentDidMount() {
console.log("component mountedw " + this.props.currentWordLetters)
// enable draggables to be dropped into this
interact(".inner-dropzone").dropzone({
// only accept elements matching this CSS selector
accept: ".draggable",
// Require a 75% element overlap for a drop to be possible
overlap: 0.75,
// listen for drop related events:
ondropactivate: function (event) {
// add active dropzone feedback
event.target.classList.add("drop-active");
},
ondragenter: function (event) {
var draggableElement = event.relatedTarget;
var dropzoneElement = event.target;
// feedback the possibility of a drop
dropzoneElement.classList.add("drop-target");
draggableElement.classList.add("can-drop");
//draggableElement.textContent = 'Dragged in'
},
ondragleave: function (event) {
// remove the drop feedback style
event.target.classList.remove("correct");
event.target.classList.remove("incorrect");
event.target.classList.remove("drop-target");
event.relatedTarget.classList.remove("can-drop");
event.target.classList.remove("test");
//event.relatedTarget.textContent = 'Dragged out'
},
ondrop: (event)=> {
event.stopImmediatePropagation();
this.checkIfLetterIsCorrect(event)
},
ondropdeactivate: function (event) {
// remove active dropzone feedback
event.target.classList.remove("drop-active");
event.target.classList.remove("drop-target");
event.target.classList.remove("drop-target");
},
})
}
render() {
return (
<Grid>
<Grid.Row style={{ justifyContent: 'space-evenly' }}>
{this.props.currentWordLetters.length > 1
? this.props.currentWordLetters.map((zone) => (
<Dropzone
key={zone}
letter={zone}
style={{
color: 'rgba(0,0,0,0)',
width: '55px',
height: '55px',
border: 'dotted white 2px',
}}
className="inner-dropzone"
/>
))
: 'Loading Dropzone...'}
</Grid.Row>
</Grid>
);
}
}
const mapDispatchToProps = (dispatch) => ({
onRoundComplete: () => dispatch(onRoundComplete()),
onCorrectLetter: () => dispatch(onCorrectLetter()),
onTimerStop: () => dispatch(onTimerStop()),
});
const mapStateToProps = (state) => ({
currentWordLetters: state.phonicsGameReducer.currentWordLetters,
});
export default connect(mapStateToProps, mapDispatchToProps)(DropzoneContainer);
I am trying to set up validations in my todo list app in react; however this doesn't seem to work. Even if the form is empty the process still goes through and I don't know where the problem is coming from. I am just following a tutorial online since I'm pretty new to this and don't know how to do it myself.
import React from "react";
import * as TodoActions from "../actions/TodoActions";
import TodoStore from "../stores/TodoStore";
import Todo from './Todo.js'
import './Todos.css'
const formValid = ({ formErrors, ...rest }) => {
let valid = true;
Object.values(formErrors).forEach(val => {
val.length > 0 && (valid = false);
});
Object.values(rest).forEach(val => {
val === null && (valid = false);
});
return valid;
};
export default class Todos extends React.Component {
constructor() {
super();
this.state = {
todos: TodoStore.getAll(),
loading: true,
formErrors: {
todo: ""
}
};
TodoActions.receiveTodos()
}
componentWillMount() {
TodoStore.addChangeListener(this.getTodos);
}
componentWillUnmount() {
TodoStore.removeChangeListener(this.getTodos);
}
componentDidUpdate() {
TodoActions.receiveTodos();
}
getTodos = () => {
this.setState({
todos: TodoStore.getAll(),
loading: false
});
}
deleteTodo = (id) => {
TodoActions.deleteTodo(id);
}
addItem = (e) => {
e.preventDefault();
TodoActions.createTodo(this._inputElement.value)
}
handleChange = e => {
e.preventDefault();
const { name, value } = e.target;
let formErrors = { ...this.state.formErrors };
switch (name) {
case "todo":
formErrors.todo =
value.length < 0 ? "Task cannot be empty" : "";
break;
default:
break;
}
this.setState({ formErrors, [name]: value }, () => console.log(this.state));
}
render() {
const { todos } = this.state;
const { formErrors } = this.state;
let TodoComponents;
if (this.state.loading) {
TodoComponents = <h1>Loading...</h1>;
} else if(todos.length) {
TodoComponents = todos.map((todo) => {
return (
<div key={todo.id} className="todo-list">
<Todo key={todo.id} name={todo.name}/>
<div className="todo-btn"><a type="button" onClick={() => this.deleteTodo(todo.id)} className="delete-btn"><i class="fas fa-trash-alt"></i></a></div>
</div>
)
});
} else {
TodoComponents = <p>No tasks to show :)</p>
}
return (
<div className="main-container">
<div className="small-container">
<h1 className="title">All Tasks</h1>
<ul>{TodoComponents}</ul>
<form onSubmit={this.addItem}>
<input ref={(a) => this._inputElement = a} placeholder="Enter Task" className="input-form {formErrors.todo.length < 0 ? 'error' : null}"/>
{formErrors.todo.length < 0 && (
<span className="errorMessage">{formErrors.firstName}</span>
)}
<button type="submit" className="input-btn">Add</button>
</form>
</div>
</div>
);
}
}
My code works but I feel like there's a way to do this without declaring a ton of state.
When the nav is clicked, it opens all SectionHeaders, and when one of those SectionHeaders is clicked, it opens the SubSections (only one SubSection allowed to be opened at once)
isFilterOpen
Open but subs closed
One sub open (only one at a time, they toggle)
Right now, my code looks like this:
class MobileFilter extends React.Component {
constructor(props) {
super(props);
this.state = {
isFilterOpen: false,
isSectionOpen: {
Business: false,
Resource: false,
Need: false,
Implementation: false,
Type: false,
Foundations: false,
Advantage: false,
Advanced: false,
Catalyst: false,
Team: false,
},
};
this.filterBar = React.createRef();
}
handleFilterClick = () => {
const {
isFilterOpen
} = this.state;
this.setState({
isFilterOpen: !isFilterOpen,
});
};
handleSectionClick = title => {
let selectedSection = title;
if (title.split(' ').length > 1) {
selectedSection = title.split(' ')[0]; // eslint-disable-line
}
this.setState(prevState => {
const newState = {};
Object.keys(prevState.isSectionOpen).forEach(key => {
newState[key] = false;
});
newState[selectedSection] = !prevState.isSectionOpen[selectedSection];
return {
...prevState,
isSectionOpen: {
...newState,
},
};
});
};
render() {
const { isFilterOpen } = this.state;
const {
need = '',
implementation = '',
type = '',
customerStoriesURL = '',
vertical,
} = this.props;
const filterClasses = isFilterOpen
? 'showMobileSections'
: 'hideMobileSections';
const wrapperClass = isFilterOpen
? 'mobileFilterWrapperActive'
: 'mobileFilterWrapper';
const filterData = this.getData(vertical);
if (vertical === 'services') {
return (
<div className="filterBarMobile" ref={this.filterBar}>
<div className="mobileFilterWrapperContainer">
<div className={wrapperClass}>
<button
type="button"
onClick={this.handleFilterClick}
className="filterHead"
>
Navigate Hub
</button>
<div className={filterClasses}>
{this.renderSections('Foundations', filterData.Foundations)}
</div>
<div className={filterClasses}>
{this.renderSections('Advantage', filterData.Advantage)}
</div>
<div className={filterClasses}>
{this.renderSections('Advanced', filterData.Advanced)}
</div>
<div className={filterClasses}>
{this.renderSections('Catalyst', filterData.Catalyst)}
</div>
<div className={filterClasses}>
{this.renderSections(
'Team Edition',
filterData['Team Edition'],
)}
</div>
</div>
</div>
</div>
);
}
return (
<div className="filterBarMobile" ref={this.filterBar}>
<div className="mobileFilterWrapperContainer">
<div className={wrapperClass}>
<button
type="button"
onClick={this.handleFilterClick}
className="filterHead"
>
Navigate Hub
</button>
<div className={filterClasses}>
{this.renderSections(need, filterData.need)}
</div>
{implementation ? (
<div className={filterClasses}>
{this.renderSections(implementation, filterData.implementation)}
</div>
) : null}
<div className={filterClasses}>
{this.renderSections(type, filterData.type)}
</div>
<div className={filterClasses}>
<div className="sectionTab">
<Link className="sectionLabel" to={customerStoriesURL}>
Customer Stories
</Link>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default MobileFilter;
As you can see, there's way too much state going on -- there as to be a way to make this more founded on the data / props that are coming in and not in a way that requires me listing out all of the SubSections as a nested state.
Any ideas would help. Thanks!
i think i've found the solution. i needed to start from scratch. here's what i have:
import React, { Component } from 'react';
import { Link } from 'gatsby';
import Search from '../Search';
import { businessData } from './filterData';
import './newFilter.less';
class NewFilter extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false,
openSubSection: '',
};
}
handleClick = () => {
const { isOpen } = this.state;
if (!isOpen) {
this.setState({
openSubSection: '',
});
}
this.setState({
isOpen: !isOpen,
});
};
handleSubClick = (e, title) => {
const { openSubSection } = this.state;
if (openSubSection === title) {
this.setState({
openSubSection: '',
});
} else {
this.setState({
openSubSection: title,
});
}
};
// renderLinks = sublevels => sublevels.map(({ title }) => <div>{title}</div>);
renderLinks = sublevels =>
sublevels.map(({ url_slug, title }) => {
if (!url_slug) {
return (
<div className="sectionLabelSub" key={title}>
{title}
</div>
);
}
return (
<Link
className="mobileSubLinks"
key={url_slug}
to={`/${url_slug}/`}
style={{ display: 'block' }}
>
{title}
</Link>
);
});
renderSection = section => {
const { isOpen, openSubSection } = this.state;
const { title, sublevels } = section;
let sectionClass = 'hideMobileSections';
let sectionOpen = 'sectionTabClosed';
let subSectionClass = 'hideMobileContent';
let arrowClass = 'arrow arrow--active';
if (isOpen) {
sectionClass = 'showMobileSections';
}
if (openSubSection === title) {
subSectionClass = 'showMobileContent';
sectionOpen = 'sectionTabOpen';
arrowClass = 'arrow';
}
// const sectionClass = isOpen ? 'section__open' : 'section__closed';
return (
<div className={sectionClass}>
<button
onClick={e => this.handleSubClick(e, title)}
type="button"
key={title}
className={sectionOpen}
>
<button type="button" className="sectionLabel">
{title}
</button>
<div className={arrowClass} />
</button>
<div className={subSectionClass} role="button" tabIndex="0">
{this.renderLinks(sublevels)}
</div>
</div>
);
};
renderSections = sections =>
sections.map(section => this.renderSection(section));
render() {
const { isOpen } = this.state;
const { navTitle, sections } = businessData;
let wrapperClass = 'mobileFilterWrapper';
if (isOpen) {
wrapperClass = 'mobileFilterWrapperActive';
}
return (
<div className="filterBarMobile" ref={this.filterBar}>
<Search vertical='business' />
<div className="mobileFilterWrapperContainer">
<div className={wrapperClass}>
<button
onClick={() => this.handleClick()}
type="button"
className="filterHead"
>
{navTitle}
</button>
{this.renderSections(sections)}
</div>
</div>
</div>
);
}
}
export default NewFilter;
basically i let the data inform the components, pass in the title to the button and the click event, and then the class looks to see if the title from the data matches the title (string) attached to the state
I'm working on an app that keeps track of salespeople's availability based on being either "Available" or "With Client".
Here's the bug I'm having. I'll use an example:
2 salespeople have been added to the app. The order in which they have been added to the app seems to matter in a way I don't expect to. For example, if the first salesperson I've added is James and the second is Rick, If I click on the button next to Rick that reads "Helped A Customer", James will now populate both the "Available" and the "With Client" tables, and Rick will have disappeared.
However if I click on them in a certain order, it works fine. For example, in the same situation as the example above, if I click on James' "Helped A Customer" first, then Rick's "Helped A Customer", then James' "No Longer With Customer", then Rick's "No Longer With Customer", it behaves as expected.
Here's the github project, you can clone it and try it out yourselves:
https://github.com/jackson-lenhart/salesperson-queue
I'll post what I think is the most relevant code here as well:
main.js:
import React from "react";
import { render } from "react-dom";
import shortid from "shortid";
import deepCopy from "deep-copy";
import AddForm from "./add-form";
import Available from "./available";
import WithClient from "./with-client";
class Main extends React.Component {
constructor() {
super();
this.state = {
queue: {
available: [],
withClient: [],
unavailable: []
},
currName: ""
};
this.addToQueue = this.addToQueue.bind(this);
this.removeFromQueue = this.removeFromQueue.bind(this);
this.handleInput = this.handleInput.bind(this);
this.move = this.move.bind(this);
}
addToQueue(name) {
let newQueue = deepCopy(this.state.queue);
newQueue.available = this.state.queue.available.concat({
name,
id: shortid.generate()
});
this.setState({
queue: newQueue
});
}
removeFromQueue(id) {
let newQueue = deepCopy(this.state.queue);
for (let k in this.state.queue) {
newQueue[k] = this.state.queue[k].filter(x =>
x.id !== id
);
}
this.setState({
queue: newQueue
});
}
move(id, from, to) {
this.setState(prevState => {
let newQueue = deepCopy(prevState.queue);
let temp = newQueue[from].find(x => x.id === id);
newQueue[from] = prevState.queue[from].filter(x =>
x.id !== id
);
newQueue[to] = prevState.queue[to].concat(temp);
return {
queue: newQueue
};
});
}
handleInput(event) {
this.setState({
currName: event.target.value
});
}
render() {
return (
<div>
<AddForm
addToQueue={this.addToQueue}
handleInput={this.handleInput}
currName={this.state.currName}
/>
<Available
available={this.state.queue.available}
move={this.move}
removeFromQueue={this.removeFromQueue}
/>
<WithClient
withClient={this.state.queue.withClient}
move={this.move}
removeFromQueue={this.removeFromQueue}
/>
</div>
);
}
}
render(
<Main />,
document.body
);
add-form.js:
import React from "react";
class AddForm extends React.Component {
constructor() {
super();
this.clickWrapper = this.clickWrapper.bind(this);
}
clickWrapper() {
this.props.addToQueue(this.props.currName);
}
render() {
return (
<div>
<input
type="text"
onChange={this.props.handleInput}
/>
<button onClick={this.clickWrapper}>
<strong>Add To Queue</strong>
</button>
</div>
);
}
}
export default AddForm;
available.js:
import React from "react";
import Salesperson from "./salesperson";
class Available extends React.Component {
render() {
const style = {
item: {
padding: "10px"
},
available: {
padding: "20px"
}
};
let available;
this.props.available.length === 0 ?
available = (
<p>None available.</p>
) : available = this.props.available.map(x =>
<div key={x.id} style={style.item}>
<Salesperson
key={x.id}
id={x.id}
name={x.name}
move={this.props.move}
removeFromQueue={this.props.removeFromQueue}
parent={"available"}
/>
</div>
);
return (
<div style={style.available}>
<h1>Available</h1>
{available}
</div>
);
}
}
export default Available;
salesperson.js:
import React from "react";
import DeleteButton from "./delete-button";
import HelpedButton from "./helped-button";
import NlwcButton from "./nlwc-button";
class Salesperson extends React.Component {
render() {
const style = {
name: {
padding: "10px"
},
button: {
padding: "5px"
}
};
let moveButton;
switch(this.props.parent) {
case "available":
moveButton = (
<HelpedButton
move={this.props.move}
id={this.props.id}
style={style.button}
/>
);
break;
case "withClient":
moveButton = (
<NlwcButton
move={this.props.move}
removeFromQueue={this.props.removeFromQueue}
id={this.props.id}
style={style.button}
/>
);
break;
default:
console.error("Invalid parent:", this.props.parent);
}
return (
<div>
<span style={style.name}>{this.props.name}</span>
{moveButton}
<DeleteButton
removeFromQueue={this.props.removeFromQueue}
name={this.props.name}
id={this.props.id}
style={style.button}
/>
</div>
);
}
}
export default Salesperson;
helped-button.js:
import React from "react";
class HelpedButton extends React.Component {
constructor() {
super();
this.clickWrapper = this.clickWrapper.bind(this);
}
clickWrapper() {
this.props.move(this.props.id, "available", "withClient");
}
render() {
return (
<span style={this.props.style}>
<button onClick={this.clickWrapper}>
<strong>Helped A Customer</strong>
</button>
</span>
);
}
}
export default HelpedButton;
This was just a typo on my part. No longer an issue. Here's the commit that fixed the typo:
https://github.com/jackson-lenhart/salesperson-queue/commit/b86271a20ac8b700bec1e15e001b0c6ef57adb8b
I am working on a simple version of ReactDND before I implement this code into my image uploader.
Each time an image is added, it is added to state and passed through to ReactDND so that it is draggable and also droppable (so users can rearrange their images).
Everything works great, except for one thing. The problem I am having is after adding multiple images, is that once I drag and drop and image (works), the State no longer updates for ReactDND and I cannot add new images.
Here is my code below (note I am just using a button to add extra items to state):
Main Component:
import React from 'react';
// Drag and drop stuff
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Container from './Container';
class ImageUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
listCount: 1
};
this.onAddItem = this.onAddItem.bind(this);
}
onAddItem(e) {
e.preventDefault();
var listArray = this.state.list;
var buildObject = {
text: 'Jeremy' + this.state.listCount.toString(),
age: '25',
id: this.state.listCount
};
listArray.push(buildObject);
let newListCount = this.state.listCount + 1;
this.setState({
list: listArray,
listCount: newListCount
});
console.log(this.state.list);
}
render() {
return (
<div>
<h1>Add to List</h1>
<button onClick={this.onAddItem}>Add Item</button>
<h1>The List</h1>
<Container id={1} list={this.state.list} />
</div>
)
}
}
export default DragDropContext(HTML5Backend)(ImageUploader);
Container:
import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';
import { DropTarget } from 'react-dnd';
class Container extends Component {
constructor(props) {
super(props);
this.state = { cards: props.list };
}
pushCard(card) {
this.setState(update(this.state, {
cards: {
$push: [ card ]
}
}));
}
removeCard(index) {
this.setState(update(this.state, {
cards: {
$splice: [
[index, 1]
]
}
}));
}
moveCard(dragIndex, hoverIndex) {
const { cards } = this.state;
const dragCard = cards[dragIndex];
this.setState(update(this.state, {
cards: {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
}
}));
}
render() {
const { cards } = this.state;
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
const style = {
width: "200px",
height: "404px",
border: '1px dashed gray'
};
const backgroundColor = isActive ? 'lightgreen' : '#FFF';
return connectDropTarget(
<div className="houzes-dropbox">
{cards.map((card, i) => {
return (
<Card
key={card.id}
index={i}
listId={this.props.id}
card={card}
removeCard={this.removeCard.bind(this)}
moveCard={this.moveCard.bind(this)} />
);
})}
</div>
);
}
}
const cardTarget = {
drop(props, monitor, component ) {
const { id } = props;
const sourceObj = monitor.getItem();
if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card);
return {
listId: id
};
}
}
export default DropTarget("CARD", cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))(Container);
Card:
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import { DragSource, DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
const style = {
border: '1px dashed grey',
padding: '0.5rem 1rem',
margin: '.5rem',
backgroundColor: 'white',
cursor: 'move'
};
class Card extends Component {
render() {
const { card, isDragging, connectDragSource, connectDropTarget } = this.props;
const opacity = isDragging ? 0 : 1;
// Background URL
let backgroundUrl = {
backgroundImage: "url(" + "http://localhost:4000/uploads/2017/8/a3ff91dc-2f80-42f7-951a-e9a74bf954d7-1200x800.jpeg" + ")"
};
console.log(card);
return connectDragSource(connectDropTarget(
<div className={`uploadedImageWrapper col-md-6 col-sm-12`}>
<div className="uploadedImage">
<span style={backgroundUrl} />
{card.text}
{card.age}
</div>
</div>
));
}
}
const cardSource = {
beginDrag(props) {
return {
index: props.index,
listId: props.listId,
card: props.card
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
if ( dropResult && dropResult.listId !== item.listId ) {
props.removeCard(item.index);
}
}
};
const cardTarget = {
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
const sourceListId = monitor.getItem().listId;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
if ( props.listId === sourceListId ) {
props.moveCard(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
monitor.getItem().index = hoverIndex;
}
}
};
export default flow(
DropTarget("CARD", cardTarget, connect => ({
connectDropTarget: connect.dropTarget()
})),
DragSource("CARD", cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}))
)(Card);
So just to recap, I can add items to state, and they become draggable and droppable. But after having dragged and dropped an element, I can no longer add anymore items to state.
Any ideas as to what the solution would be? What am I doing wrong?
Thank-you for looking through this, and any answers. Cheers.
#Notorious.
I have checked your code in my side and solved the issue.
When you drag and drop an element that changes the state of Container but not the state of ImageUploader.
So I made a function to inform the state of Container has changed.
Also I inserted componentWillReceiveProps() function to Container and updated the state of Container in that function.
Finally the problem solved.
Here's the changed code.
Main Component:
import React from 'react';
// Drag and drop stuff
import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Container from './Container';
class ImageUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
listCount: 1
};
this.onAddItem = this
.onAddItem
.bind(this);
this.listChanged = this.listChanged.bind(this);
}
onAddItem(e) {
e.preventDefault();
var listArray = this.state.list;
var buildObject = {
text: 'Jeremy' + this
.state
.listCount
.toString(),
age: '25',
id: this.state.listCount
};
listArray.push(buildObject);
let newListCount = this.state.listCount + 1;
this.setState({list: listArray, listCount: newListCount});
}
listChanged(newList) {
this.setState({
list: newList
})
}
render() {
return (
<div>
<h1>Add to List</h1>
<button onClick={this.onAddItem}>Add Item</button>
<h1>The List</h1>
<Container id={1} list={this.state.list} listChanged={this.listChanged}/>
</div>
)
}
}
export default DragDropContext(HTML5Backend)(ImageUploader);
Container:
import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';
import { DropTarget } from 'react-dnd';
class Container extends Component {
constructor(props) {
super(props);
this.state = { cards: this.props.list };
}
pushCard(card) {
this.setState(update(this.state, {
cards: {
$push: [ card ]
}
}));
}
removeCard(index) {
this.setState(update(this.state, {
cards: {
$splice: [
[index, 1]
]
}
}));
}
moveCard(dragIndex, hoverIndex) {
const { cards } = this.state;
const dragCard = cards[dragIndex];
this.setState(update(this.state, {
cards: {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
}
}));
}
componentWillReceiveProps(nextProps) {
// You don't have to do this check first, but it can help prevent an unneeded render
if (nextProps.list !== this.state.cards) {
this.props.listChanged(this.state.cards);
}
}
render() {
const { cards } = this.state;
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
const style = {
width: "200px",
height: "404px",
border: '1px dashed gray'
};
const backgroundColor = isActive ? 'lightgreen' : '#FFF';
return connectDropTarget(
<div className="houzes-dropbox">
{cards.map((card, i) => {
return (
<Card
key={card.id}
index={i}
listId={this.props.id}
card={card}
removeCard={this.removeCard.bind(this)}
moveCard={this.moveCard.bind(this)} />
);
})}
</div>
);
}
}
const cardTarget = {
drop(props, monitor, component ) {
const { id } = props;
const sourceObj = monitor.getItem();
if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card);
return {
listId: id
};
}
}
export default DropTarget("CARD", cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))(Container);
I am really happy if this helped you.
Thanks for reading my post.
Vladimir