Related
I am trying to implement TABS in my demo application.
condition : I want to render those TABS or menu item which have length less than 100px
I created tab with following steps
export const TabsContext = React.createContext({
selectedTabId: "",
setSelectedTabId: () => {},
tabsVisible: [],
tabsHidden: [],
setTabsVisible: () => {},
setTabsHidden: () => {}
});
Created a context which have selectedIndex, visibleArray, hiddenArray is present
In TABLIST component I checked all element width .if it is greater than 100 I pushed to visible else hidden.
but how to render visible array/ ITEM
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
const getWidth = () => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
hidden.push(element);
} else {
visible.push(element);
}
});
};
useEffect(() => {
getWidth();
}, []);
return (
<div>
<ul className="tabs" ref={tabsRef}>
{children}
</ul>
</div>
);
};
TabList.displayName = "Tabs.TabList";
export { TabList };
here is my whole code
https://codesandbox.io/s/intelligent-wescoff-dd3xd?file=/src/tablist.js:141-924
Getting Error while rendering
Objects are not valid as a React child (found: [object HTMLLIElement]).
I set the default value of visible array
<Tabs
defaultTabId="1"
defaultTabsVisible={data.map((i) => (
<Tabs.Tab tabId={i} key={i}>
{i}
</Tabs.Tab>
))}
>
<Tabs.TabList isSize="medium"></Tabs.TabList>
</Tabs>
and try to updated array
const getWidth = () => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
hidden.push(element);
} else {
visible.push(element);
}
});
setTabsVisible(visible);
setTabsHidden(hidden);
};
while update I am getting this error
Objects are not valid as a React child (found: [object HTMLLIElement]). I
Ok so this is what I came up with
import React, { useRef, useContext, useEffect, useState } from "react";
import classNames from "classnames";
import { TabsContext } from "./context";
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
const [firstRender, setFirstRender] = useState(true);
const getWidth = () => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element, i) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
hidden.push(children[i]);
} else {
visible.push(children[i]);
}
});
setTabsVisible(visible);
setTabsHidden(hidden);
if(tabsRef.current)
setFirstRender(false)
};
useEffect(() => {
getWidth();
}, []);
return (
<div>
<ul className="tabs" ref={tabsRef}>
{firstRender ? children : tabsVisible }
</ul>
</div>
);
};
TabList.displayName = "Tabs.TabList";
export { TabList };
This will check all children size on initial render and update the visible array. Then after the first render it will only display the visible children.
Update - aria-hidden
{firstRender ?
(<ul className="tabs" ref={tabsRef} aria-hidden={false}>
{children}
</ul>)
:
(<>
<ul className="tabs" ref={tabsRef} aria-hidden={false}>
{tabsVisible}
</ul>
<ul className="tabs-hidden" ref={tabsRef} aria-hidden={true}>
{tabsHidden}
</ul>
</>)
}
If you want aria-hidden property to be individual for each tab, you would have to check for element width inside the tab component also. Then add the aria-hidden hidden property in there.
You have to use the property opacity of CSS to hide your tabs, after this step you have to create a state tabsToDisplay to store the tabs that verify your condition.
styles.css
.tabs-hidden {
opacity: 0; // <=== hide your tabs
display: flex;
list-style: none;
}
tabList.js
import React, { useRef, useContext, useEffect, useState } from "react";
import classNames from "classnames";
import { TabsContext } from "./context";
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
const [tabsToDisplay, setTabsToDisplay] = useState([]); // <=== create this state to store the tabs that match your condition
const getWidth = () => {
let hidden = [],
visible = [],
tabsToDisplay = [];
[...tabsRef.current.children].forEach((element, i) => {
if (element.offsetWidth > 100) {
hidden.push(element);
} else {
visible.push(element);
tabsToDisplay.push(children[i]);
}
});
console.log("test", tabsToDisplay);
setTabsToDisplay(tabsToDisplay);
};
useEffect(() => {
getWidth();
}, []);
return (
<div>
<ul className="tabs-hidden" ref={tabsRef}>
{children}
</ul>
<ul className="tabs">{tabsToDisplay}</ul>
</div>
);
};
TabList.displayName = "Tabs.TabList";
export { TabList };
You can check the demo:
https://codesandbox.io/s/hungry-galileo-qk09i?file=/src/tablist.js:0-1156
As you're using ref, you can do it using them.
This is an alternative to current answers, using useLayoutEffect
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
React.useLayoutEffect(() => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
element.style.display = "none";
element.setAttribute("aria-hidden", "true");
hidden.push(element);
} else if (element.style.display !== 'none') {
element.style.display = "block";
element.setAttribute("aria-hidden", "false");
visible.push(element);
}
});
setTabsVisible(visible)
setTabsHidden(hidden)
}, [setTabsVisible, setTabsHidden, tabsRef])
return (
<div>
<ul className="tabs" ref={tabsRef}>
{children}
</ul>
</div>
);
};
Changed tabs to prevent infinite rerendering
return React.createElement(
TabsContext.Provider,
{
value: {
selectedTabId,
setSelectedTabId,
tabsVisible,
tabsHidden,
setTabsVisible,
setTabsHidden
}
},
children
);
Also note the
} else if (element.style.display !== "none") {
This prevents the element that is displayed with none, to be displayed as having width < 100 on a further rerender
https://codesandbox.io/s/pedantic-kirch-ykdc3?file=/src/tablist.js
I am trying to make a game like purple pairs of the purple palace game. What I am trying to do is that whenever two elements are clicked which are not equal in value, then the cards should automatically close but what is happening is something different. Whenever I clicked two wrong cards, then chose a different card then the value is getting changed even though I have not written any code to do so. This is very frustrating, I am getting nowhere trying to solve this. Please help me solve this.
Original Code Pen link Click here to visit
I think the problem lies somewhere in handleClick function.
function Card(props) {
const [show, setShow] = useState(props.chosen);
handleClick = (e) => {
if (props.chosen) {
setShow(true);
} else {
props.onClick();
setShow(!show);
}
};
const style1 = {
background: "grey",
transform: `rotateY(${!show ? 0 : 180}deg)`
};
const style2 = {
background: "#aaa",
transform: `rotateY(${show ? 0 : 180}deg)`
};
return (
<div class="container" onClick={handleClick}>
<div className="flip" style={style1}></div>
<div className="flip" style={style2}>
{props.value}
</div>
</div>
);
}
class GameBoard extends React.Component {
constructor() {
super();
this.state = {
score: 0,
time: 0,
list: [...generateObList(), ...generateObList()],
count: 0
};
}
handleClick = async (id) => {
await this.clickBtn(id);
const list = _.cloneDeep(this.state.list);
const current = list.find((a) => a.id === id);
for (let x of list) {
if (
x.clicked &&
x.id != id &&
x.value == list.find((a) => a.id == id).value
) {
x.chosen = true;
x.clicked = false;
current.chosen = true;
current.clicked = false;
this.setState((prev) => ({
list: prev.list.map((el) =>
el.id === id ? current : el.value === current.value ? x : el
),
score: prev.score + 1
}));
} else if (this.state.count % 2 == 0 && x.clicked) {
console.log("Entered");
current.clicked = false;
x.clicked = false;
this.setState((prev) => ({
list: prev.list.map((el) =>
el.id === id ? current : el.value === current.value ? x : el
)
}));
}
}
};
clickBtn = (id) => {
const current = _.cloneDeep(this.state.list).find((e) => e.id === id);
let deClick = current.clicked;
current.clicked = !current.clicked;
this.setState((prev) => ({
list: prev.list.map((el) => (el.id === id ? current : el)),
count: prev.count + (deClick ? -1 : 1)
}));
};
render() {
const boardStyle = {
gridTemplateColumns: `repeat(5, 1fr)`,
gridTemplateRows: `repeat(5,1r)`
};
let list = this.state.list.map((n) => (
<Card
value={n.value}
onClick={(e) => {
this.handleClick(n.id);
}}
chosen={n.chosen}
clicked={n.clicked}
/>
));
return (
<div class="gameBoard" style={boardStyle}>
{list}
</div>
);
}
}
There were some serious issues in the handleClick function. The main thing that went wrong was that you were somehow managing to replace list items with other list items.
The overwrite issue is happening in this line. I'm not entirely sure why this is causing the issue, but it is.
list: prev.list.map((el) =>
el.id === id ? current : el.value === current.value ? x : el
)
If you just replace it with the following, then the issue dissapears:
list: prev.list.map((el) => el.clicked ? {...el, clicked:false}: el)
The clickBtn wasn't an async function, so using await on it wouldn't do anything. If you want to await for the state to change, you need to resolve a promise. I haven't worked with class components in a while, so I don't know if this would be a particularly encouraged way of working with them, but there are likely other ways:
await new Promise((resolve) =>
this.setState(
(prev) => ({
list: prev.list.map((card) =>
card.clicked ? { ...card, clicked: false } : card
),
freeze: false
}),
() => resolve()
)
);
Another thing to note is that you were keeping the state of which cards were clicked in the GameBoard, so there was no reason to have Card be stateful, in fact, that's the reason why cards wouldn't flip back over.
By changing the start of cards from using useState to just the props values, that's fixed:
const show = props.chosen||props.clicked;
handleClick = (e) => {
if (props.chosen) {
} else {
props.onClick();
}
};
https://codepen.io/ZachHaber/pen/yLJGayv
Refactors:
I did some refactoring to get everything working while I was figuring out what went wrong.
I also had some fun implementing logic, which is why I added the flipping behavior with a timeout when the user guesses wrong.
const {shuffle} = _;
const numbersList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let id = 0;
const generateObList = () => {
return numbersList.map((e) => ({
// Use something more guaranteed to not be the same value
// Math.random *could* clash, but is very unlikely to.
id: id++,
value: e,
chosen: false,
clicked: false
}));
};
// Using a shuffle algorithm here to shuffle the tiles.
const generateList = () => shuffle([...generateObList(), ...generateObList()]);
function Card(props) {
const show = props.chosen || props.clicked;
// Remove the local state here, it's just problematic
// Let the parent control the state.
const handleClick = (e) => {
if (props.chosen) {
} else {
props.onClick();
}
};
const style1 = {
background: "grey",
transform: `rotateY(${!show ? 0 : 180}deg)`
};
const style2 = {
background: "#aaa",
transform: `rotateY(${show ? 0 : 180}deg)`
};
return (
<div className="container" onClick={handleClick}>
<div className="flip" style={style1}></div>
<div className="flip" style={style2}>
{props.value}
</div>
</div>
);
}
class GameBoard extends React.Component {
constructor() {
super();
this.state = {
score: 0,
time: 0,
list: generateList(),
count: 0,
freeze: false
};
}
timerId = null;
performUpdate = (id) => {
// Flip the relevant card in the list
const list = this.state.list.map((card) =>
card.id === id ? { ...card, clicked: !card.clicked } : card
);
// Get the active card
const current = list.find((card) => card.id === id);
// Get all cards that match the current value
let matches = list.filter((card) => card.value === current.value);
// Somehow the card was already chosen
// Likely can remove this because this condition is also in the children
if (matches.every((card) => card.chosen)) {
return; // invalid click, don't do anything
}
// the matches are all clicked, now they are valid to be chosen!
if (matches.every((card) => card.clicked)) {
this.setState((prev) => ({
list: list.map((card) =>
card.value !== current.value
? card
: { ...card, clicked: false, chosen: true }
),
score: prev.score + 1,
count: prev.count + 1
}));
return;
}
// There are 2 cards clicked, reset them after a timer!
if (list.filter((card) => card.clicked).length === 2) {
// Have to post the current click state change -
// Make it so it will flip over the tile briefly
this.setState((prev) => ({ list, count: prev.count + 1, freeze: true }));
// Then after a timeout, flip it back over
this.timerId = setTimeout(() => {
this.setState((prev) => ({
list: prev.list.map((card) =>
card.clicked ? { ...card, clicked: false } : card
),
freeze: false
}));
}, 500);
return;
}
// At this point it's just a normal click:
// set the new adjusted list, and increment count.
this.setState((prev) => ({
list,
count: prev.count + 1
}));
};
handleClick = (id) => {
// Waiting for board to flip tiles over currently. User is impatient
// Could just return in this case if you want users to wait
if (this.state.freeze) {
clearTimeout(this.timerId);
// Perform the update to clear the clicked status and freeze status
// and wait for it to resolve before continuing
this.setState(
(prev) => ({
list: prev.list.map((card) =>
card.clicked ? { ...card, clicked: false } : card
),
freeze: false
}),
() => this.performUpdate(id)
);
} else {
this.performUpdate(id);
}
};
reset = () => {
this.setState({
count: 0,
score: 0,
time: 0,
freeze: false,
list: generateList()
});
};
render() {
const boardStyle = {
gridTemplateColumns: `repeat(5, 1fr)`,
gridTemplateRows: `repeat(5,1r)`
};
let list = this.state.list.map((card) => (
<Card
key={card.id}
value={card.value}
onClick={(e) => {
this.handleClick(card.id);
}}
chosen={card.chosen}
clicked={card.clicked}
/>
));
return (
<div>
<div className="gameBoard" style={boardStyle}>
{list}
</div>
<div>
move count: {this.state.count}
<br />
score: {this.state.score}
</div>
{this.state.score === this.state.list.length / 2 && (
<button onClick={this.reset}>Reset</button>
)}
</div>
);
}
}
ReactDOM.render(<GameBoard/>,document.getElementById('root'))
.container {
position: relative;
width: 100px;
height: 100px;
background: #eee;
cursor: pointer;
}
.gameBoard {
display: grid;
}
.container .flip {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
font-size: 4em;
transition: transform 400ms;
transition-timing-function: cubic-bezier(0.1, 0.39, 0.3, 0.95);
backface-visibility: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<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>
<div id="root"/>
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
I can't seem to pass this handler correctly. TabItem ends up with undefined for onClick.
SearchTabs
export default class SearchTabs extends Component {
constructor(props) {
super(props)
const breakpoints = {
[SITE_PLATFORM_WEB]: {
displayGrid: true,
autoFocus: true,
},
[SITE_PLATFORM_MOBILE]: {
displayGrid: false,
autoFocus: false,
},
};
this.state = {
breakpoints,
filters: null,
filter: null,
isDropdownOpen: false,
selectedFilter: null,
tabs: null,
};
this.tabChanged = this.tabChanged.bind(this);
this.closeDropdown = this.closeDropdown.bind(this);
}
... more code
createTabs(panels) {
if(!panels) return;
const tabs = panels.member.map((panel, idx) => {
const { selectedTab } = this.props;
const { id: panelId, headline } = panel;
const url = getHeaderLogo(panel, 50);
const item = url ? <img src={url} alt={headline} /> : headline;
const classname = classNames([
searchResultsTheme.tabItem,
(idx === selectedTab) ? searchResultsTheme.active : null,
]);
this.renderFilters(panel, idx, selectedTab);
return (
<TabItem
key={panelId}
classname={classname}
idx={idx}
content={item}
onClick={this.tabChanged(idx, headline)}
/>
);
});
return tabs;
}
tabChanged(idx, headline) {
const { selectedTab } = this.props;
const { selectedFilter } = this.state;
const selectedFilterIdx = _.get(selectedFilter, 'idx', null);
if (selectedTab !== idx) {
this.props.resetNextPage();
this.props.setTab(idx, selectedFilterIdx, headline);
this.closeDropdown();
}
}
render() {
// const { panels, selectedTab } = this.props;
// if (!panels || panels.length === 0) return null;
//
//
// const { tabs, selectedTab } = this.props;
return (
<div>
<ul>{this.state.tabs}</ul>
</div>
);
}
}
export const TabItem = ({ classname, content, onClick, key }) => (
<li key={key} className={`${classname} tab-item`} onClick={onClick} >{content}</li>
);
so in TabItem onClick={onClick} ends up with undefined for onClick.
More info
here's how this used to work, when this was a function in the parent Container:
// renderDefaultTabs() {
// const { panels, selectedTab } = this.props;
//
// if (!panels || panels.length === 0) return;
//
// let filter = null;
//
// const tabs = panels.member.map((panel, idx) => {
// const { id: panelId, headline } = panel;
// const url = getHeaderLogo(panel, 50);
// const item = url ?
// <img src={url} alt={headline} /> : headline;
// const classname = classNames([
// searchResultsTheme.tabItem,
// (idx === selectedTab) ? searchResultsTheme.active : null,
// ]);
//
// filter = (idx === selectedTab) ? this.renderFilters(panel) : filter;
//
// return (
// <li
// key={panelId}
// className={classname}
// onClick={() => {
// this.tabChanged(idx, headline);
// }}
// >
// {item}
// </li>
// );
// });
So I extracted that out to that SearchTabs including moving the tabChange d method to my new SearchTabs component. And now in the container the above now does this:
renderDefaultTabs() {
const {
onFilterClick,
panels,
resetNextPage,
selectedTab,
selectedFilter,
isDropdownOpen,
} = this.props;
return (<SearchTabs
panels={panels}
...
/>);
}
Note: renderDefaultTabs() is sent as a prop to in the render() of the container and the Search calls it back thus rendering it in the Search's render():
Container
render() {
return (
<Search
request={{
headers: searchHeaders,
route: searchRoute,
}}
renderTabs={this.renderDefaultTabs}
renderSearchResults={this.renderSearchResults}
handleInputChange={({ input }) => {
this.setState({ searchInput: input });
}}
renderAltResults={true}
/>
);
}
Search is a shared component our apps use.
Update
So I mentioned that the Container's render() passes the renderDefaultTabs function as a prop to <Search />. Inside <Search /> it ultimately does this: render() { <div>{renderTabs({searchResults})}</div>} which calls the container's renderDefaultTabs function which as you can see above, ultimately renders
So it is passing it as a function. It's just strange when I click a TabItem, it doesn't hit my tabChanged function whatsoever
Update
Christ, it's hitting my tabChanged. Errr..I think I'm good. Thanks all!
onClick={this.tabChanged(idx, headline)}
This is not a proper way to pass a function to child component's props. Do it like (though it is not recommended)
onClick={() => this.tabChanged(idx, headline)}
UPDATE
I want to add more explanation. By onClick={this.tabChanged(idx, headline)}, you are executing tabChanged and pass its returned value to onClick.
With your previous implementation: onClick={() => { this.tabChanged(idx, headline); }}, now onClick will be a function similar to:
onClick = {(function() {
this.tabChanged(idx, headline);
})}
So it works with your previous implementation.
With your new implementation, onClick={() => this.tabChanged(idx, headline)} should work
I am using draftjs editor. I could render the content but I could not show images. How can i show image when using draftjs? Right now the url is only shown instead of images.The server sends the data as following
img src="http://image_url" style="argin:30px auto; max-width: 350px;"
Sorry i could not use img tag html way so excluded the tag syntax.
function findImageEntities(contentBlock, callback, contentState) {
contentBlock.findEntityRanges(character => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === "IMAGE"
);
}, callback);
}
const Image = props => {
const { height, src, width } = props.contentState
.getEntity(props.entityKey)
.getData();
return <img src={src} height={height} width={width} />;
};
class AdminEditor extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
editorContent: undefined,
contentState: "",
touched: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.htmlMarkup !== this.props.htmlMarkup) {
const content = nextProps.htmlMarkup;
const blocksFromHTML = convertFromHTML(content);
const plainState = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap
);
this.setState(state => ({
editorState: EditorState.createWithContent(plainState, decorator)
}));
}
}
onEditorStateChange = editorState => {
this.setState({
editorState
});
};
onEditorChange = editorContent => {
this.setState({
editorContent
});
};
handleChange = event => {
this.props.setEditorState(
this.state.editorState.getCurrentContent().hasText() && this.state.touched
);
};
render() {
const { editorState } = this.state;
const { stateOfEditor } = this.props;
return (
<div>
<Editor
tabIndex={0}
editorState={editorState}
initialContentState={this.props.htmlMarkup}
toolbarClassName="home-toolbar"
onEditorStateChange={this.onEditorStateChange}
toolbar={{
history: { inDropdown: true },
inline: { inDropdown: false },
link: { showOpenOptionOnHover: true },
image: {
uploadCallback: this.imageUploadCallBack,
defaultSize: { height: "auto", width: "50%" }
}
}}
onContentStateChange={this.onEditorChange}
onChange={this.handleChange}
/>
</div>
);
}
}
export default AdminEditor;
exact copy of decorator is in top of the findImageEntities which i haven't pasted just to reduce the number of lines of code
I saw the props onEditorStateChange={this.onEditorStateChange} . I doubt you're using the draft-js-wysiwyg not draft-js.
In draft-js-wysiwyg , u can visit here :
https://github.com/jpuri/react-draft-wysiwyg/issues/589