I'm having issues integrating Draft.js with Material UI's TextField component (or Input). I followed the documentation (https://material-ui.com/components/text-fields/#integration-with-3rd-party-input-libraries) but it still fails. I've integrated another package using the same method before which worked but there are issues with integrating Draft.js.
Code below:
import React, { useState, useRef, forwardRef } from "react";
import Editor, { createEditorStateWithText } from "draft-js-plugins-editor";
import createEmojiPlugin from "draft-js-emoji-plugin";
import { TextField, makeStyles } from "#material-ui/core";
import { isObject } from "lodash";
import { findDOMNode } from "react-dom";
import classNames from 'classnames';
function InputWrapper(props) {
const { component: Component, inputRef, ...other } = props;
const ref = useRef();
React.useImperativeHandle(inputRef, () => ({
focus: () => {
findDOMNode(inputRef.current).focus();
},
}));
return <Component ref={ref} {...other} />;
}
const emojiPlugin = createEmojiPlugin();
const { EmojiSuggestions, EmojiSelect } = emojiPlugin;
const plugins = [emojiPlugin];
const text = `Cool, we can have all sorts of Emojis here. 🙌
🌿☃️🎉🙈 aaaand maybe a few more here 🐲☀️🗻 Quite fun!`;
const useStyles = makeStyles((theme) => ({
textField: {
width: "100%",
minHeight: 31,
tabSize: 8,
fontVariantLigatures: "none",
boxSizing: "content-box",
userSelect: "text",
whiteSpace: "pre-wrap",
},
}));
let DraftInput = (props, ref) => {
const {
InputProps,
InputLabelProps,
value,
handleEnterKeyPress,
onChange,
id,
...other
} = props;
const [editorState, setEditorState] = useState(
createEditorStateWithText(text)
);
const baseClasses = useStyles();
const handleChange = (state) => {
setEditorState(state);
// onChange(state);
};
const propClass =
isObject(InputProps) && InputProps.hasOwnProperty("className")
? InputProps.className
: false;
const classes = classNames([baseClasses, propClass, "emoji-input"]);
return (
<div>
<TextField
inputRef={ref}
autoFocus={true}
value={value}
InputProps={{
inputComponent: InputWrapper,
inputProps: {
component: Editor,
onChange: handleChange,
editorState,
},
...InputProps,
}}
InputLabelProps={{
...InputLabelProps,
}}
{...InputProps}
{...other}
/>
</div>
);
};
DraftInput = forwardRef(DraftInput);
export default DraftInput;
Note: there's still test data being used.
This is how I got it to work.
import Editor from "#draft-js-plugins/editor";
const DraftField = React.forwardRef(function DraftField(props, ref) {
const { component: Component, editorRef, handleOnChange, ...rest } = props;
React.useImperativeHandle(ref, () => ({
focus: () => {
editorRef?.current?.focus();
},
}));
return <Component {...rest} ref={editorRef} onChange={handleOnChange} />;
});
const DraftEditor = () => {
const [editorState, setEditorState] = React.useState();
const editorRef = React.useRef(null);
const handleOnChange = (newEditorState) => {
setEditorState(newEditorState);
};
return (
<TextField
fullWidth
label="Content"
InputProps={{
inputProps: {
component: Editor,
editorRef,
editorState,
handleOnChange
},
inputComponent: DraftField,
}}
/>
);
};
Related
The code below is using antd v3,for the antd v4 the form.create() is already not available, I read the documentation,it shows to replace the form.create() by useform.I tried to put it in the code(the second code) but still not working. Hhow can I use useForm in the code?
import React from 'react'
import {Form, Input} from 'antd'
const EditableContext = React.createContext();
const EditableRow = ({ form, index, ...props }) => (
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
);
export const EditableFormRow = Form.create()(EditableRow);
export class EditableCell extends React.Component {
state = {
editing: false,
};
toggleEdit = () => {
const editing = !this.state.editing;
this.setState({ editing }, () => {
if (editing) {
this.input.focus();
}
});
};
save = e => {
const { record, handleSave } = this.props;
this.form.validateFields((error, values) => {
if (error && error[e.currentTarget.id]) {
return;
}
this.toggleEdit();
handleSave({ ...record, ...values });
});
};
}
The useForm code
export const EditableFormRow = () => {
const { form, index, ...props } = useForm();
return (
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
)
};
useForm() return array, you can get form like this:
import React from "react";
import ReactDOM from "react-dom";
import { Form, Input, Button } from "antd";
const App = () => {
return <>{["a#x.com", "b#x.com"].map((email) => EditRow({ email }))}</>;
};
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 }
};
const tailLayout = {
wrapperCol: { offset: 8, span: 16 }
};
const EditRow = ({ email }) => {
const [form] = Form.useForm();
const onFinish = (values) => {
console.log(values);
};
const onClick = () => {
console.log(form.getFieldValue("email"));
};
return (
<Form form={form} layout={layout} onFinish={onFinish}>
<Form.Item
name="email"
label="email"
rules={[{ required: true }]}
initialValue={email}
>
<Input />
</Form.Item>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button onClick={onClick}>getFieldValue</Button>
</Form.Item>
</Form>
);
};
https://codesandbox.io/s/large-data-array-with-redux-toolkit-forked-7tp63?file=/demo.tsx
In the given codesandbox example I am using react typescript with redux-toolkit
in codesandbox example. I am trying to bind countries with checkbox and textbox.
When I check on checkboxes it looks slow and textbox edit also feels very slow.
some time it breaks the page.
I am not sure what I am doing wrong.
You should make CountryItem it's own component and make it a pure component:
import React, { FC, useEffect } from "react";
import { createStyles, Theme, makeStyles } from "#material-ui/core/styles";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import { countryList } from "./dummyData";
import { Checkbox, Grid, TextField } from "#material-ui/core";
import { useAppDispatch, useAppSelector } from "./store/hooks";
import {
setCountries,
setCountrySelected,
setCountryValue
} from "./store/slice/geography-slice";
import { Country } from "./interface/country.modal";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: "100%",
backgroundColor: theme.palette.background.paper
}
})
);
const CountryItem: FC<{ country: Country }> = ({ country }) => {
console.log('render item',country.name)
const dispatch = useAppDispatch();
const handleCheckboxChange = (
event: React.ChangeEvent<HTMLInputElement>,
country: Country
) => {
const selectedCountry = { ...country, isSelected: event.target.checked };
dispatch(setCountrySelected(selectedCountry));
};
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedCountry = { ...country, value: event.target.value };
dispatch(setCountryValue(selectedCountry));
};
return (
<ListItem button>
<Checkbox
checked={country?.isSelected ?? false}
onChange={(event) => {
handleCheckboxChange(event, country);
}}
/>
<ListItemText primary={country.name} />
<TextField value={country?.value ?? ""} onChange={handleTextChange} />
</ListItem>
);
};
const PureCountryItem = React.memo(CountryItem)
export default function SimpleList() {
const classes = useStyles();
const dispatch = useAppDispatch();
const { countries } = useAppSelector((state) => state.geography);
useEffect(() => {
dispatch(setCountries(countryList));
}, []);
return (
<div className={classes.root}>
<Grid container>
<Grid item xs={6}>
<List component="nav" aria-label="secondary mailbox folders">
{countries.map((country, index) => (
<PureCountryItem country={country} key={`CountryItem__${index}`} />
))}
</List>
</Grid>
</Grid>
</div>
);
}
The following code caused useEffect() to be called infinitely. Why is that the case? Removing drawingboarditems from the useEffect dependencies array solves the infinite loop, but as a result DrawingBoard will not automatically rerender whenenver a user adds an item to the database.
DrawingBoard.jsx
export default function DrawingBoard() {
const [drawingboarditems, setdrawingboarditems] = useState([]);
const currentUser = useContext(CurrentUserContext);
const [loading, setLoading] = useState(true);
const classes = useStyles();
useEffect(() => {
if (currentUser) {
const items = [];
//retrieving data from database
db.collection("drawingboarditems")
.where("userID", "==", currentUser.id)
.get()
.then((query) => {
query.forEach((doc) => {
items.push({
id: doc.id,
...doc.data(),
});
});
setdrawingboarditems(items);
setLoading(false);
});
}
}, [currentUser, drawingboarditems]);
return (
<>
{loading == false ? (
<Container>
<Masonry
breakpointCols={breakpoints}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{drawingboarditems.map((item) => (
<div>
<Note item={item} />
</div>
))}
<Note form />
</Masonry>
</Container>
) : (
<div className={classes.root}>
<CircularProgress />
</div>
)}
</>
);
Note.jsx
import React from "react";
import { Card, CardHeader, IconButton, makeStyles } from "#material-ui/core";
import MoreVertIcon from "#material-ui/icons/MoreVert";
import Form from "./Form";
import CardWindow from "./CardWindow";
const useStyles = makeStyles((theme) => ({
card: {
backgroundColor: theme.palette.secondary.main,
margin: theme.spacing(1, 0),
},
}));
export default function Note({ item, form }) {
const classes = useStyles();
return (
<Card className={classes.card}>
{form ? (
<Form />
) : (
<CardHeader
action={
<CardWindow/>
}
title={item.title}
/>
)}
</Card>
);
}
Form.jsx
import React, { useContext } from "react";
import TextField from "#material-ui/core/TextField";
import { makeStyles } from "#material-ui/core/styles";
import AddCircleOutlineIcon from "#material-ui/icons/AddCircleOutline";
import IconButton from "#material-ui/core/IconButton";
import { db } from "../FireStore";
import { CurrentUserContext } from "../utils/Context";
const useStyles = makeStyles((theme) => ({
form: {
"& .MuiTextField-root": {
margin: theme.spacing(1),
width: "70%", // 70% of card in drawing board
},
},
}));
export default function Form() {
const classes = useStyles();
const [value, setValue] = React.useState("");
const currentUser = useContext(CurrentUserContext);
const handleChange = (event) => {
setValue(event.target.value);
};
const handleSubmit = (event) => {
if (value) {
event.preventDefault();
db.collection("drawingboarditems").add({
title: value,
userID: currentUser.id,
});
setValue("");
}
};
return (
<form className={classes.form} noValidate autoComplete="off">
<div>
<TextField
id="standard-textarea"
placeholder="Add item"
multiline
onChange={handleChange}
value={value}
/>
<IconButton aria-label="add" onClick={handleSubmit}>
<AddCircleOutlineIcon fontSize="large" />
</IconButton>
</div>
</form>
);
}
In your case I would move the logic to the DrawingBoard component, and would pass props to the children, so when a children adds an item, the main component would know to refresh the list of items.
Example (not tested):
Extract the logic to work with FireBase to functions. In that way they would be more re-usable, and would not add clutter to your code.
const drawingboarditemsCollection = 'drawingboarditems';
function getAllNotes(userID) {
return db.collection(drawingboarditemsCollection)
.where("userID", "==", userID)
.get()
.then((query) => {
return query.map(doc => {
items.push({
id: doc.id,
...doc.data(),
});
});
});
}
function addNote(userID, title) {
return db.collection(drawingboarditemsCollection).add({
title,
userID,
});
}
The DrawingBoard component should handle the connection with the server, and should pass functions as props to children:
export default function DrawingBoard() {
const [drawingboarditems, setdrawingboarditems] = useState([]);
const currentUser = useContext(CurrentUserContext);
const [loading, setLoading] = useState(true);
const classes = useStyles();
// add the logic to get notes by
const getNotes = useCallback(() => {
setLoading(true);
getAllNotes(currentUser.id)
.then(items => {
setdrawingboarditems(items);
})
.finally(=> {
setLoading(false);
});
}, [currentUser]);
// create the submit handler
const handleSubmit = value => {
addNote(currentUser.id, value)
.then(getNotes); // after adding a note update the items
}
// initial get notes, or when currentUser changes
useEffect(() => {
getNotes();
}, [getNotes]);
return (
<>
{loading == false ? (
<Container>
<Masonry
breakpointCols={breakpoints}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{drawingboarditems.map((item) => (
<div>
<Note item={item} />
</div>
))}
<Note form onSubmit={handleSubmit} />
</Masonry>
</Container>
) : (
<div className={classes.root}>
<CircularProgress />
</div>
)}
</>
);
}
Pass the onSubmit function to Form:
export default function Note({ item, form, onSubmit }) {
const classes = useStyles();
return (
<Card className={classes.card}>
{form ? (
<Form onSubmit={onSubmit} />
) : (
<CardHeader
action={
<CardWindow/>
}
title={item.title}
/>
)}
</Card>
);
}
Use onSubmit to handle the submission:
export default function Form({ onSubmit }) {
const classes = useStyles();
const [value, setValue] = React.useState("");
const handleChange = (event) => {
setValue(event.target.value);
};
const handleSubmit = (event) => {
if (value) {
event.preventDefault();
onSubmit(value); // let the parent handle the actual update
setValue("");
}
};
return ( ... );
}
I am using react +redux+typescript+firebase for a simple todo application. How to move all actions from components to store if i use react hooks(without other third party libraries, if it is possible).
Components/Todo.tsx:
import React from 'react';
import {connect} from 'react-redux';
import Switch from '#material-ui/core/Switch';
import IconButton from '#material-ui/core/IconButton';
import DeleteIcon from '#material-ui/icons/Delete';
import "./Todo.scss";
import {todosRef} from "../../firebase/firebase";
const Todo = (props: any) => {
const classes = [''];
const { todo } = props;
const updateTodo = () => {
todosRef.child(todo.id).set({...todo,done:!todo.done})
}
if (todo.done) {
classes.push('_completed');
}
return (
<div className="Todo">
<Switch
edge="end" checked={todo.done} onChange={updateTodo}
inputProps={{ "aria-labelledby": "switch-list-label-bluetooth" }}
/>
<p className={classes.join(' ')}>{todo.task}</p>
<IconButton aria-label="delete" onClick={e => todosRef.child(todo.id).remove()}>
<DeleteIcon fontSize="large" />
</IconButton>
</div>
);
}
export default connect()(Todo);
components/TodoForm.tsx
import React, { useState } from "react";
import TextField from '#material-ui/core/TextField';
import {todosRef} from "../../firebase/firebase";
const TodoForm:React.FC = () => {
const [title, setTitle] = useState<string>("");
const createTodo = (e: React.FormEvent<EventTarget>) => {
e.preventDefault();
const item = {
task: title,
done: false,
};
todosRef.push(item);
setTitle("");
};
return (
<form onSubmit={createTodo}>
<TextField
style={{ width: "100%" }}
id="outlined-basic"
value={title}
onChange={(e:React.ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}
label="Write task"
variant="outlined"
/>
</form>
);
}
export default TodoForm;
components/TodoList.tsx:
import React, {useState, useEffect} from "react";
import Todo from "../Todo/Todo";
import Divider from '#material-ui/core/Divider';
import {todosRef} from "../../firebase/firebase";
const TodoList:React.FC = () => {
const [todos,setTodos] = useState<any>([]);
useEffect(() => {
todosRef.on('value', (snapshot) => {
let items = snapshot.val();
let newState = [];
for (let item in items) {
newState.push({
id: item,
task: items[item].task,
done: items[item].done
});
}
setTodos(newState)
});
},[])
return (
<>
{todos.map((todo: any, i: number) => (
<React.Fragment key={todo.id}>
<Todo todo={todo} />
{i<todos.length -1 && <Divider />}
</React.Fragment>
))}
</>
);
}
export default TodoList;
actions/index.ts:
export const fetchTodos = (todo:any) => {
return {
type: 'FETCH_TODOS',
payload: todo,
}
}
export const addToDo = (setTodos:any) => {
return {
type: 'ADD_TODO',
payload: setTodos,
}
}
export const updateTodo = (todo:any) => {
return {
type: 'UPDATE_TODO',
payload: todo,
}
}
export const removeTodo = (todo:any) => {
return {
type: 'REMOVE_TODO',
payload: todo,
}
}
I started describing the actions, but I don't know exactly what to write in the context of the store.
const initialState = {
todos: [],
setTodos: [],
}
export default (state = initialState, action:any) => {
switch (action.type) {
case FETCH_TODOS:
return {
...state,
todo:action.payload
};
case REMOVE_TODO:
...
default:
return state;
...
I will also be grateful if you can indicate with a couple of examples (instead of any) which types I should use, since I'm still new to typescript
I am fetching list of testsuites using api call on component mount. Api returns list in chronological order.
Setting them as options for a select dropdown(Material-UI).
Then set the selected option to latest testSuite and using its Id get the corresponding testSuite data.
Data is retrieved successfully and pie chart is getting displayed.
Api calls are working fine and React dev tools shows the selectedTestSuite value to be set correctly.But DOM doesn't show the selection in the select dropdown.
Can someone please advise what is the mistake I am doing in this code? Thanks in advance.
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { Doughnut } from 'react-chartjs-2';
import { makeStyles } from '#material-ui/styles';
import axios from 'axios';
import { useSpring, animated } from 'react-spring';
import '../../Dashboard.css';
import MenuItem from '#material-ui/core/MenuItem';
import {
Card,
CardHeader,
CardContent,
Divider,
TextField,
} from '#material-ui/core';
import CircularProgress from '#material-ui/core/CircularProgress';
const useStyles = makeStyles(() => ({
circularloader: {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
},
actions: {
justifyContent: 'flex-end',
},
inputField: {
width: '150px',
},
}));
const TestSuiteVsScanCount = (props) => {
const { className, ...rest } = props;
const classes = useStyles();
const [doughData, setDoughData] = useState([]);
const [dataLoadedFlag, setDataLoadedFlag] = useState(false);
const [testSuites, setTestSuites] = useState([]);
const [selectedTestSuite, setSelectedTestSuite] = useState({});
useEffect(() => {
function getTestSuites() {
axios.get('http://localhost:3000/api/v1/testsuite/12').then((resp) => {
setTestSuites(resp.data.reverse());
});
}
getTestSuites();
}, []);
useEffect(() => {
if (testSuites.length > 0) {
setSelectedTestSuite(() => {
return {
type: testSuites[0].TestSuiteName,
id: testSuites[0].TestSuiteId,
};
});
}
}, [testSuites]);
useEffect(() => {
function getTestSuiteData() {
let doughData = [];
if (selectedTestSuite.id) {
axios
.get(
'http://localhost:3000/api/v1/summary/piechart/12?days=30&testsuiteid=' +
selectedTestSuite.id,
)
.then((resp) => {
resp.data.forEach((test) => {
doughData = [test.TestCount, test.ScanCount];
});
setDoughData({
labels: ['Test Count', 'Scan Count'],
datasets: [
{
data: doughData,
backgroundColor: ['#FF6384', '#36A2EB'],
hoverBackgroundColor: ['#FF6384', '#36A2EB'],
},
],
});
setDataLoadedFlag(true);
});
}
}
getTestSuiteData();
}, [selectedTestSuite]);
const ChangeType = (id) => {
testSuites.forEach((suite) => {
if (suite.TestSuiteId === id) {
setSelectedTestSuite({
type: suite.TestSuiteName,
id: suite.TestSuiteId,
});
}
});
};
return (
<Card {...rest} className={clsx(classes.root, className)}>
<CardHeader
action={
<TextField
select
label="Select Test Suite"
placeholder="Select Tests"
value={selectedTestSuite.id}
className={classes.inputField}
name="tests"
onChange={(event) => ChangeType(event.target.value)}
variant="outlined"
InputLabelProps={{
shrink: true,
}}
>
{testSuites.map((testSuite) => (
<MenuItem
key={testSuite.TestSuiteId}
value={testSuite.TestSuiteId}
>
{testSuite.TestSuiteName}
</MenuItem>
))}
</TextField>
}
title="Test Suite vs Scan Count"
/>
<Divider />
<CardContent>
<div>
{dataLoadedFlag ? (
<Doughnut data={doughData} />
) : (
<CircularProgress
thickness="1.0"
size={100}
className={classes.circularloader}
/>
)}
</div>
</CardContent>
<Divider />
</Card>
);
};
TestSuiteVsScanCount.propTypes = {
className: PropTypes.string,
};
export default TestSuiteVsScanCount;
I was able to fix this issue with the help of my colleague by setting the initial state of selectedTestSuite to {type:'', id:0} instead of {}.
Changed this
const [selectedTestSuite, setSelectedTestSuite] = useState({});
To this
const [selectedTestSuite, setSelectedTestSuite] = useState({type:'', id:0});
But I am not sure why this worked.
I believe that the main problem is when you pass a value to TextField component with undefined, the TextField component will assume that is an uncontrolled component.
When you set you initial state for selectedTestSuite to be {} the value for selectedTestSuite.id will be undefined. You can find value API reference in https://material-ui.com/api/text-field/