So, I used Draft-js to create blog posts. When a user creates a post, the data is converted to a string and sent to the server to be saved. I converted the draft-js EditorState like this:
JSON.stringify(convertToRaw(editorState.getCurrentContent())).
Now, I want to add an edit post function. To do this, I get the string data from the server (in the exact same format as described above), and I try to create an editorState from it like this:
let data = convertFromRaw(res.data['postContent'])
setEditorState(EditorState.createWithContent(data))
This seems to work as when I run console.log(convertToRaw(editorState.getCurrentContent())) I can see that the server data is in the editorState. However, nothing is displayed inside of the editor. My question: How do I show the data to the user inside the editor, and make it editable? As in, the user can see the post and modify parts of it.
Here is a screenshot of what I get (as you can see, the title is there but the editor is empty when it should say "Test data!"):
Here is my full code:
import React, {useEffect, useState} from 'react'
import { EditorState, convertToRaw, convertFromRaw } from 'draft-js'
import { useLocation } from 'react-router-dom';
import { Editor } from 'react-draft-wysiwyg';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import './css/CreateBlog.css'
import ApiClient from '../util/ApiClient';
// submit the blog post
const submitPage = async (data, postTitle, setPosted, setPostFailed) => {
const content = JSON.stringify(convertToRaw(data));
ApiClient.post("/blog/handle-blog", {"postTitle": postTitle,"postContent": content})
.then((res) => {
console.log(res)
// there was no error
if(res.status === 201) {
setPosted("Your blog has been posted.")
setPostFailed(false)
} else {
setPosted("There was an error.")
setPostFailed(true)
}
})
}
// if this is an edit
const getPostData = async (postID, setEditorState, setPostTitle) => {
const res = await ApiClient.get(`/blog/get-blog/${postID}`)
// set the title and data
setPostTitle(res.data['postTitle'])
setEditorState(EditorState.createWithContent(convertFromRaw(res.data['postContent']))) // set the editorState data
}
export default function CreateBlogpost() {
const location = useLocation();
const postID = location.state?.id; // get the ID if there is one
const [editorState, setEditorState] = useState(null);
const [postTitle, setPostTitle] = useState("");
const [posted, setPosted] = useState(""); // the error/success message
const [postFailed, setPostFailed] = useState(false); // has the blog successfully been posted?
// handle the post button click
const handleSubmit = async (e) => {
e.preventDefault();
// check the post contains data
if(editorState === EditorState.createEmpty() || postTitle == "") {
setPosted("Please add some content to your post.")
setPostFailed(true)
return;
}
const data = editorState.getCurrentContent()
await submitPage(data, postTitle, setPosted, setPostFailed);
}
useEffect(() => {
// if the state has an ID, then you're editing a post
if(postID) {
getPostData(postID, setEditorState, setPostTitle)
}
}, [])
return(
<div className="create-blog">
<div className="create-blog-text m-2 mb-3">
<h1>{postID ? "Edit" : "Create"} a blog post</h1>
</div>
<div className="d-flex">
<h3 className="m-2">Title: </h3>
<input value={postTitle} type="text" id="fname" className="m-2" onChange={e=>setPostTitle(e.target.value)} />
<p className="m-2">Please note: if you want to upload a photo, upload it to an image sharing site like imgur and then link the image.</p>
</div>
<div className="editor-container mb-3 ml-2 mr-2"
style={{ border: "1px solid black", minHeight: "6em", cursor: "text" }}
>
<Editor value={editorState} onEditorStateChange={setEditorState} />
</div>
<button className="button-3 m-2" onClick={(e) => handleSubmit(e)}>
{postID ? "Edit" : // if there is a postID, then you are editing the post
"Post" }
</button>
{/* <button className="button-3 m-2" onClick={(e) => handleSubmit(e)}>Save as draft</button> */}
<h4 style={{ color: postFailed ? 'red' : 'green'}}>{posted}</h4>
</div>
)
}
change
<Editor
value={editorState}
onEditorStateChange={setEditorState}
/>
to
<Editor
editorState={editorState}
onEditorStateChange={setEditorState}
/>
Here is the working version in sandbox. I commented the useLocation and ApiClient calls so perhaps those are the culprit. Also your res.data['postContent'] looks like a JSON. If so, then you need to JSON.parse(res.data['postContent']).
Related
I'm new to React.js and I'm actually trying to create a page structured in the following way:
-A form to insert [version,date,image,content] and post a JSON via POST request (with Axios)
-A display part in which I GET the same data and display on screen by clicking on a button
actually I'm able to do this by introducing the Get and Post logic in the used component.
In order to use Components and have a clear code, i would to have a separate component to call inside the various components to make a GET or POST request.
By using hooks I'm not able to do this. The actual code is:
This is UserGet.js
import axios from "axios";
import {useState} from "react";
const baseURL = "http://localhost:8088/";
const userURL ="changelog/version.txt";
function UserGet(props) {
const [get, setGet] = useState(null);
axios.get(baseURL+userURL).then((response) => {
setGet(response.data);
});
return [get, setGet] ;
}
export default UserGet;
while in the component I want to use as data displayer:
const DisplayInfo =(props) => {
const [items, setItems] = useState([]);
const onFinish = (() => {
setItems(UserGet());
})
const DisplayData = items.map(
(info)=>{
return(
<div className='changelogDisplay' key={info.version}>
<button className='version' >v {info.version} </button>
<div className='datelog' > {info.data} </div>
<div className='dataHeader' > {info.title} </div>
<div className='whatsnew' > {info.text} </div>
<div className='imageLog' alt='Changelog pic' >{info.img} </div>
</div>
)
});
return(
<div>
<Form onFinish={onFinish}>
<Form.Item>
<Button type="primary" htmlType="submit" name='Refresh'> Refresh list</Button>
<div className='displayData'>
{DisplayData}
</div>
</Form.Item>
</Form>
</div>
)
}
export default DisplayInfo;
Actually, if I use
const response = UserGet();
I'm able to get data and insert into items for further process. I want to get the data using Onfinish or Onclick properties but I'm not able due to Hooks that cannot stay inside a function. How can I get a solution?
I tried different ways, and I don't want to use Redux at the moment in order to improve my knowledge on this.
Hooks are not so simple to me
Currently you're setting the items state to the [get, setGet] value that returns from the UserGet hook.
Here you return the data back from the request.
async function UserGet(props) {
const response = await axios.get(baseURL + userURL);
return response.data;
}
Then in the onFinish
const onFinish = (() => {
const newItems = UserGet();
setItems(newItems);
// or
// setItems(UserGet());
});
I hope this helps you with your project!
I'm working on the comments feature of my assignment. I'm trying to display the author name of the comments from my object. However, my map function doesnt seem to be working as whenever I click the button I get an error saying Error: Objects are not valid as a React child (found: object with keys {roles, _id, username, email, password, __v}). If you meant to render a collection of children, use an array instead.
Reviews,js
import React, {useState, useRef} from 'react';
import Axios from 'axios';
import {Button, Input} from 'antd';
import authService from '../../services/auth.service'
import authHeader from '../../services/auth-header';
import FirstReview from './FirstReview';
const {TextArea} = Input;
const Reviews = (props) => {
const currentUser = authService.getCurrentUser();
const [review, setReview] = useState('');
const handleChange = (e) => {
setReview(e.target.value)
}
const onSubmit = (e) => {
e.preventDefault();
const variables = {
movieId: props.movieId,
content: review,
author: currentUser.id,
reviewId: props.reviewId,
}
Axios.post('http://localhost:8080/api/review/addReview', variables,{ headers: authHeader()})
.then(response=> {
if(response.data.success) {
setReview("")
props.refreshFunction(response.data.result)
} else {
alert('Failed to save review')
}
})
}
return (
<div>
<p>Reviews</p>
{props.reviewList && props.reviewList.map((review, index) => (
(!review.responseTo &&
<React.Fragment key={review._id}>
<FirstReview review={review} movieId={props.movieId} refreshFunction={props.refreshFunctions}/>
</React.Fragment>
)))}
<form style={{display: 'flex'}} onSubmit>
<TextArea
style={{width: '100%', borderRadius: '5px'}}
placeholder = "leave a review"
value={review}
onChange={handleChange}
/>
<Button style = {{width: '20%', height: '52px'}} onClick={onSubmit}></Button>
</form>
</div>
);
};
export default Reviews
FirstReview.js
import React from 'react'
import { Button, Comment, Form, Header, TextArea } from 'semantic-ui-react'
const action = [
<span onClick key="comment-basic-reply-to">reply</span>
]
function FirstReview(props) {
// const authorName = props.review.author;
// const author = {
// authorName: authorName
// }
return (
<div>
<Comment>
<Comment.Content>
<Comment.Author as='a'> {props.review.author} </Comment.Author>
</Comment.Content>
</Comment>
<form style={{display: 'flex'}} onSubmit>
<TextArea
style={{width: '100%', borderRadius: '5px'}}
placeholder = "leave a review"
value={Comment}
onChange
/>
</form>
</div>
)
}
export default FirstReview
I have a file called movie-info.component which has a container which uses the Review component.
<Container2>
<Reviews refreshFunction={updateReview} reviewList={reviewList} movieId={movieInfo?.id}/>
</Container2>
This is an assignment question so I can't give you the full answer, but what your error message means is that you're taking an object somewhat this:
let thing = { first_name: "Tom", last_name: "Jack" }
and then doing this:
<p>{thing}</p>
and although you might be expecting to see "Tom Jack" in the above, you will get the error that you've suggested because React is unable to turn the object into anything that would meaningfully fit inside a p tag.
Do this instead:
<p>{thing.first_name + thing.last_name}</p>
which is specific enough for React to validly render.
I have a simple Input type= file. I am capturing the the selected file in state. Once a file is selected and the clear button is clicked it does not clear the state. This causes {selectedFile.name} to throw an undefined error when the user clicks cancel during the next file selection.
Is there a way to make my clear button actually clear selectedFile state? or a way to handle the error so that a file can still be selected?
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import AddIcon from "#mui/icons-material/Add";
import { withStyles } from "#material-ui/core/styles";
import Fab from "#mui/material/Fab";
const styles = () => ({
root: {
boxSizing: "border-box",
margin: "0",
padding: "0"
},
textInput: {
textAlign: "center"
},
test: {
fontSize: "3rem"
}
});
const CampaignForm = props => {
const { classes } = props;
const [selectedFile, setSelectedFile] = useState();
const [isFileSelected, setIsFileSelected] = useState(false);
const handleClear = event => {
setIsFileSelected(false);
setSelectedFile("");
console.log(selectedFile.name);
};
const handleFileSelection = event => {
setSelectedFile(event.target.files[0]);
setIsFileSelected(true);
};
return (
<div className={classes.root}>
<h1 className={classes.textInput}>Text Campaign</h1>
<div className={classes.textInput}>
<label htmlFor='upload-csv'>
<input
style={{ display: "none" }}
id='upload-csv'
disabled={isFileSelected}
name='upload-csv'
type='file'
onChange={handleFileSelection}
/>
<Fab
color='primary'
size='small'
disabled={isFileSelected}
component='span'
aria-label='add'
variant='extended'>
<AddIcon /> Add CSV
</Fab>
</label>
<br />
{isFileSelected ? (
<div>
<br />
<button onClick={handleClear}>clear</button>
<p>{selectedFile.name}</p>
</div>
) : (
<p>No file added</p>
)}
</div>
</div>
);
};
CampaignForm.propTypes = { classes: PropTypes.object.isRequired };
export default withStyles(styles)(CampaignForm);
enter image description here
First You need to set the default null value to selectedFile state.
const [selectedFile, setSelectedFile] = useState(null);
Then, in handleClear Function set,
setSelectedFile(null);
and final the condition should be
{isFileSelected ? (
<div>
<br />
<button onClick={handleClear}>clear</button>
<p>{selectedFile.name}</p>
</div>
) : (
selectedFile === null && <p>No file added</p>
)}
[optional]:
the default file select event does not select the same file again, so if you need to allow the user to select the same file then add following property in the input tag:
onClick={(event) => {
event.target.value = null;
}}
you have to do, in handleClear function, im guessing that setSelectedFile(event.target.files[0]); is json object not string,
setSelectedFile({});
as you are setSelectedFile(""); setting string to it, string dont have .name only json have that
<p>{selectedFile.name}</p>
so "".name name would be undefined, which is correct, your fix would be this, hope it helps
const handleClear = event => {
setIsFileSelected(false);
setSelectedFile({});
console.log(selectedFile.name);
};
I'm creating a page where I can get an overview of all my notes/summaries.
The note's pages are markdown files converted into HTML used in a dynamic file (this works).
On the notes pages (with all the notes listed), I want to implement a search function:
I need to find a way to get the value of my search input into the getServerSideProps(). This way it can filter out the notes and only display the ones that you searched for.
Things to note:
When I change the string 'searchTerm' in getServerSideProps() in the note page the search function works.
const searchTerm = "" ;
I can't move the getAllNotes() outside the getServerSideProps(), because the imports of the data file can't be accessed on the client-side.
const allNotes = getAllNotes();
The searchbar needs to be centered, I know.
Code for the note page:
import Head from 'next/head';
import Link from 'next/link';
import Footer from '../../components/Footer.js';
import Navigation from '../../components/Navigation';
import { Rating } from '#material-ui/core';
import Chip from '../../components/Chip.js';
import noteStyle from '../../styles/Notes.module.css';
import styles from '../../styles/Home.module.css';
import { getAllNotes } from '../../lib/data';
import Search from '../../components/Search';
export default function Notes({ notes }) {
return (
<div id="top" className={styles.container}>
<Head></Head>
<main className={styles.main}>
<section>
<Navigation />
</section>
<section className={noteStyle.noteSection}>
<h1>Notes & Summaries</h1>
<Search />
<div className={noteStyle.notes}>
{notes.map((note) => (
<Link key={note.slug} href={`/note/${note.slug}`}>
<a key={note.slug} className={noteStyle.noteCard}>
<h2 key="title">{note.title}</h2>
<h3 key="author">{note.author}</h3>
<p key="abstract">
{note.abstract}
</p>
<div className={noteStyle.aboutNote}>
<Rating key="rating" className={noteStyle.rating} name="half-rating-read" defaultValue={note.rating} precision={0.5} readOnly />
<Chip key="label" className={noteStyle.noteChip} bgColor={note.labelColors[0]} text={note.labelText[0]} icon={note.labelIcons[0]} />
</div>
</a>
</Link>
))}
</div>
</section>
</main>
<Footer />
</div>
)
}
export async function getServerSideProps() {
const allNotes = getAllNotes();
const searchTerm = "";
//Searches in title, author & abstract data field for a match with the query
const searchNotes = allNotes.filter((note) => {
return note.data.title.toLowerCase().includes(searchTerm.toLowerCase()) || note.data.author.toLowerCase().includes(searchTerm.toLowerCase()) || note.data.abstract.toLowerCase().includes(searchTerm.toLowerCase())
});
return {
props: {
//Here data serialising (dates, urls, ...),
notes: searchNotes.map(({ data, content, slug }) => ({
...data,
content,
slug,
})),
},
};
};
Code for the search component:
import { useState } from 'react';
export default function Search() {
const [input, setInput] = useState('');
const search = (e) => {
setInput(e.target.value);
console.log("You searched", input);
}
return (
<div className={style.search}>
<SearchIcon className={style.search__inputIcon}/>
<input type="text" placeholder="Search for a note..." value={input} onChange={search} />
</div>
)
}
Code for the getAllNotes (this function converts all the markdown files into HTML):
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const contentDirectory = path.join(process.cwd(), '_content');
export function getAllNotes() {
const allNotes = fs.readdirSync(contentDirectory);
return allNotes.map(fileName => {
const slug = fileName.replace('.md', '');
const fileContents = fs.readFileSync(
path.join(contentDirectory, fileName),
'utf-8'
)
const {content, data} = matter(fileContents);
return {
data,
content,
slug,
};
})
}
Research I used while building the page:
https://www.youtube.com/watch?v=gQIv-biWxjo&t=1291s&ab_channel=HarryWolff
https://nextjs.org/docs/basic-features/data-fetching
https://medium.com/#matswainson/building-a-search-component-for-your-next-js-markdown-blog-9e75e0e7d210
To make a value from the client-side code available to getServerSideProps you can pass a query param on the URL with a router.push.
// In your `Notes` or `Search` component
router.push('/notes?searchTerm=hello');
You can then access the value from the query param in getServerSideProps.
export async function getServerSideProps(context) {
const searchTerm = context.query.searchTerm ?? "" // `context.query.searchTerm` would contain "hello"
// ...
}
I am trying to use react-quilljs to implement text editor in a full mern stack app, but I am not sure what part of the quill object should I save to the database in the backend. I have the follow code for the editor:
import React, { useState } from 'react';
import { useQuill } from 'react-quilljs';
// or const { useQuill } = require('react-quilljs');
import 'quill/dist/quill.snow.css'; // Add css for snow theme
// or import 'quill/dist/quill.bubble.css'; // Add css for bubble theme
export default () => {
const { quill, quillRef } = useQuill();
const [content, setContent] = useState('');
console.log(quill); // undefined > Quill Object
console.log(quillRef); // { current: undefined } > { current: Quill Editor Reference }
React.useEffect(() => {
if (quill) {
quill.on('text-change', () => {
setContent(?) // What part of the quill object I need to use here to set the content before sending it to the backend <-----------------
console.log(quill.getFormat());
console.log('Text change!');
});
}
}, [quill]);
const submitHandler = (e) => {//here I will send the editor content to the bakcend};
return (
<div style={{ width: 500, height: 300 }}>
<div ref={quillRef} />
<form onSubmit={submitHandler}>
<button type='submit'>Submit</button>
</form>
</div>
);
};
You should save two things separately one is Raw data of the Quill and another is extracting only text from it
Refer to the sandbox below , you need to save the content
https://codesandbox.io/s/react-quill-demo-forked-0qr5f?file=/src/editor.js