draft.js - retrieve formatted text from db - reactjs

My draft.js <TextEditor /> populates body with the text e.g: '{"blocks":[{"key":"3mont","text":"lorem ipsum","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}' and persists it to the db after using convertToRaw().
In Post.js, I want to retrieve and display the formatted text from the db.
I've read that in order to do this, I must use convertToRaw() and then convertFromRaw() when retrieving it from the db but I'm having the same problems as this (I'm receiving the cors error and Unexpected token u in JSON at position 0) whenever I use convertFromRaw() and try to retrieve the formatted text from the db.
I've set up my server to support cors so why am I receiving the cors error? Is it because I am trying to parse an invalid response into JSON?
How can I get the formatted text from the db in Post.js?
Any help would be really appreciated!
GitHub
CreatePost.js
class CreatePost extends React.Component {
constructor(props) {
super(props);
this.state = {
title: "",
body: EditorState.createEmpty(),
};
}
changeHandler = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
submitHandler = (e) => {
e.preventDefault();
const {
user: { _id },
} = isAuthenticated();
axios({
url: `${API}/post/new-post/${_id}`,
method: "POST",
data: {
...this.state,
body: JSON.stringify(convertToRaw(this.state.body.getCurrentContent())),
},
})
.then((response) => {
// this.setState({ createdPost: this.state.title });
return response
})
.catch((error) => {
if (!this.state.title || !this.state.body) {
this.setState({
error: "This post must contain a title and a body.",
});
}
console.log(error);
});
};
// Attempt to map through blocks
//getText = () => {
// const {body} = this.state;
//const arr = body.blocks.map(({ text }) => text).join(' ')
// console.log(arr)
//}
render() {
const { title, body } = this.state;
return (
<>
<Navbar />
<Tabs>
<TabList>
<Tab>Draft</Tab>
<Tab>Preview</Tab>
</TabList>
<TabPanel>
<div>
<form onSubmit={this.submitHandler}>
<div>
// title input
</div>
<div>
<TextEditor
onChange={(value) => this.setState({ body: value })}
editorState={body}
/>
</div>
<button type="submit">
Publish
</button>
</form>
</div>
</TabPanel>
<TabPanel>
<div>
<h1>{title}</h1>
// display body text value here too
{this.getText()}
</div>
</TabPanel>
</Tabs>
</>
);
}
}
Post.js (display body text)
const [post, setPost] = useState({});
const [error, setError] = useState(false);
const id = props.match.params.id;
const loadSinglePost = (slug, id) => {
read(slug, id).then((data) => {
if (error) {
console.log(data.error);
setError(data.error);
} else {
setPost(data)
console.log(data);
}
});
};
useEffect(() => {
const slug = props.match.params.slug;
loadSinglePost(slug, id);
}, [props]);
return (
<>
<div>
<h3>{post.title}</h3>
...
// display text value below
<p>{post.body}</p>
</div>
</div>
</>
);
};
TextEditor.js
class TextEditor extends React.Component {
constructor(props) {
super(props);
this.plugins = [addLinkPlugin];
}
toggleBlockType = (blockType) => {
this.props.onChange(RichUtils.toggleBlockType(this.props.editorState, blockType));
};
handleKeyCommand = (command) => {
const newState = RichUtils.handleKeyCommand(
this.props.editorState,
command
);
if (newState) {
this.props.onChange(newState);
return "handled";
}
return "not-handled";
};
onUnderlineClick = () => {
this.props.onChange(
RichUtils.toggleInlineStyle(this.props.editorState, "UNDERLINE")
);
};
onBoldClick = (event) => {
this.props.onChange(RichUtils.toggleInlineStyle(this.props.editorState, "BOLD"));
};
onItalicClick = () => {
this.props.onChange(
RichUtils.toggleInlineStyle(this.props.editorState, "ITALIC")
);
};
onAddLink = () => {
const editorState = this.props.editorState;
const selection = editorState.getSelection();
const link = window.prompt("Paste the link -");
if (!link) {
this.props.onChange(RichUtils.toggleLink(editorState, selection, null));
return "handled";
}
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity("LINK", "MUTABLE", {
url: link,
});
const newEditorState = EditorState.push(
editorState,
contentWithEntity,
"create-entity"
);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
this.props.onChange(RichUtils.toggleLink(newEditorState, selection, entityKey));
};
toggleBlockType = (blockType) => {
this.props.onChange(RichUtils.toggleBlockType(this.props.editorState, blockType));
};
render() {
return (
<div>
// formatting buttons
<div>
<Editor
blockStyleFn={getBlockStyle}
editorState={this.props.editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.props.onChange}
plugins={this.plugins}
placeholder="Post Content"
/>
</div>
</div>
);
}
}

Apparently draft-js does not have html output function because it's supposed to have no assumption on the output so people can tune their output however they want (see this). This means we'll have to implement it ourselves and if you're looking for just an html or markdown output to preserve in the database, then this mono repo can be of help. I've implemented an example of how to do it in this sandbox. Note that I used dangerouslySetInnerHTML for demonstration which is not optimal. You may want to use sanitization and rich text components to display back the posts. As a matter of fact I'd suggest ditching html and going for markdown instead if possible.

Related

retrieve text from db using draft.js

My draft.js <TextEditor /> populates body with the text e.g: '{"blocks":[{"key":"3mont","text":"lorem ipsum","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}'.
In Post.js, I want to retrieve and display the formatted text from the db. I've read that in order to do this, I must use convertToRaw() and then convertFromRaw() when retrieving it from the db but I'm having the same problems as this (I'm receiving the cors error and Unexpected token u in JSON at position 0) whenever I use convertFromRaw() and try to retrieve the formatted text from the db.
I have setup my server to support cors so is it because I am trying to parse an invalid response into JSON? Why am I getting the cors error and how can I solve it?
GitHub
How can I get the formatted text from the db in Post.js? Any help would be really appreciated!
CreatePost.js
class CreatePost extends React.Component {
constructor(props) {
super(props);
this.state = {
title: "",
body: EditorState.createEmpty(),
};
}
changeHandler = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
submitHandler = (e) => {
e.preventDefault();
const {
user: { _id },
} = isAuthenticated();
axios({
url: `${API}/post/new-post/${_id}`,
method: "POST",
data: {
...this.state,
body: JSON.stringify(convertToRaw(this.state.body.getCurrentContent())),
},
})
.then((response) => {
// this.setState({ createdPost: this.state.title });
return response
})
.catch((error) => {
if (!this.state.title || !this.state.body) {
this.setState({
error: "This post must contain a title and a body.",
});
}
console.log(error);
});
};
// Attempt to map through blocks
//getText = () => {
// const {body} = this.state;
//const arr = body.blocks.map(({ text }) => text).join(' ')
// console.log(arr)
//}
render() {
const { title, body } = this.state;
return (
<>
<Navbar />
<Tabs>
<TabList>
<Tab>Draft</Tab>
<Tab>Preview</Tab>
</TabList>
<TabPanel>
<div>
<form onSubmit={this.submitHandler}>
<div>
// title input
</div>
<div>
<TextEditor
onChange={(value) => this.setState({ body: value })}
editorState={body}
/>
</div>
<button type="submit">
Publish
</button>
</form>
</div>
</TabPanel>
<TabPanel>
<div>
<h1>{title}</h1>
// display body text value here too
{this.getText()}
</div>
</TabPanel>
</Tabs>
</>
);
}
}
Post.js (display body text)
const [post, setPost] = useState({});
const [error, setError] = useState(false);
const id = props.match.params.id;
const loadSinglePost = (slug, id) => {
read(slug, id).then((data) => {
if (error) {
console.log(data.error);
setError(data.error);
} else {
setPost(data)
console.log(data);
}
});
};
useEffect(() => {
const slug = props.match.params.slug;
loadSinglePost(slug, id);
}, [props]);
return (
<>
<div>
<h3>{post.title}</h3>
...
// display text value below
<p>{post.body}</p>
</div>
</div>
</>
);
};
TextEditor.js
class TextEditor extends React.Component {
constructor(props) {
super(props);
this.plugins = [addLinkPlugin];
}
toggleBlockType = (blockType) => {
this.props.onChange(RichUtils.toggleBlockType(this.props.editorState, blockType));
};
handleKeyCommand = (command) => {
const newState = RichUtils.handleKeyCommand(
this.props.editorState,
command
);
if (newState) {
this.props.onChange(newState);
return "handled";
}
return "not-handled";
};
onUnderlineClick = () => {
this.props.onChange(
RichUtils.toggleInlineStyle(this.props.editorState, "UNDERLINE")
);
};
onBoldClick = (event) => {
this.props.onChange(RichUtils.toggleInlineStyle(this.props.editorState, "BOLD"));
};
onItalicClick = () => {
this.props.onChange(
RichUtils.toggleInlineStyle(this.props.editorState, "ITALIC")
);
};
onAddLink = () => {
const editorState = this.props.editorState;
const selection = editorState.getSelection();
const link = window.prompt("Paste the link -");
if (!link) {
this.props.onChange(RichUtils.toggleLink(editorState, selection, null));
return "handled";
}
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity("LINK", "MUTABLE", {
url: link,
});
const newEditorState = EditorState.push(
editorState,
contentWithEntity,
"create-entity"
);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
this.props.onChange(RichUtils.toggleLink(newEditorState, selection, entityKey));
};
toggleBlockType = (blockType) => {
this.props.onChange(RichUtils.toggleBlockType(this.props.editorState, blockType));
};
render() {
return (
<div>
// formatting buttons
<div>
<Editor
blockStyleFn={getBlockStyle}
editorState={this.props.editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.props.onChange}
plugins={this.plugins}
placeholder="Post Content"
/>
</div>
</div>
);
}
}

Filtering items with dropdown in react

So,I am creating a bookstore app.In that user can search and filter the books.The filtering of books works for one time and next it goes empty.I know where i was doing wrong.After filtering I was setting the state to the filtered state.How to correct it???
Here is the code....
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
books: [],
userInput: "",
};
}
componentDidMount() {
this.setState({
books: BOOK_DATA,
});
}
handleInputChange = (event) => {
this.setState({
userInput: event.target.value,
});
};
handleCategoryChange = (event) => {
console.log(event.target.value.toLowerCase());
const books = [...this.state.books];
const categorizedBooks = books.filter((book) => {
return book.category === event.target.value.toLowerCase();
});
//Problem here
this.setState({
books: categorizedBooks,
});
};
render() {
const { books, userInput } = this.state;
//This one is for searching books
const searchedBook = books.filter((book) => {
return book.name.toLowerCase().includes(userInput);
});
return (
<div className="App">
<Header />
<SearchBox change={(event) => this.handleInputChange(event)} />
<CategoriesandFilter
handleCategoryChange={(event) => this.handleCategoryChange(event)}
/>
<ShopTitle />
<BooksComponent books={searchedBook} />
</div>
);
}
}
export default App;
you are using the already filtered data for the next filter. you need to use the unfiltered data.
...
const categorizedBooks = BOOK_DATA.filter((book) => { // Use your unfiltered data here (BOOK_DATA)
return book.category === event.target.value.toLowerCase();
});
...

ReactJS – how to check whether value is undefined

I am creating a find nearest restaurant app using Google Places API. I am calling for a background image in my ResultItem component in a prop <ResultsItem name={places.name} image={Thumbnail} rating={places.rating} rating_total={places.user_ratings_total} />
This image is defined as Thumbnail in const above this part of the code. My code runs smoothly but as soon as places.photos[0] returns as undefined (meaning that it is Google Place that doesn't have any image uploaded) i get an error saying:
Unhandled Rejection (TypeError): Cannot read property '0' of undefined
I think what I have to do is check whether places.photos[0] is undefined or not but I do not seem to get it right...
My goal is to display another placeholder image when this value turns out undefined. If it is defined though the component should take the image from google places api.
Can someone help me?
FULL COMPONENT:
import React, { Component } from 'react';
// Imports
import axios from 'axios';
import Script from 'react-load-script';
import Placeholder from './Placeholder.jsx';
import FadeIn from 'react-fade-in';
import {
Spinner,
Paragraph,
SideSheet,
Tooltip,
IconButton,
SearchInput
} from 'evergreen-ui';
import ResultsItem from '../../components/ResultsItem/ResultsItem.jsx';
import Geocode from 'react-geocode';
// Styles
import './Search.scss';
class Autocomplete extends Component {
// Define Constructor
constructor(props) {
super(props);
// Declare State
this.state = {
type: 'restaurant',
radius: 10,
lat: '59.0738',
lng: '41.3226',
city: '',
query: '',
open: false,
places: [],
place_detail: [],
sidebar: false,
loading: true
};
this.currentLocationOnClick = this.currentLocationOnClick.bind(this);
this.handlePlaceSelect = this.handlePlaceSelect.bind(this);
}
currentLocationOnClick = async () => {
let { lat, lng, places } = this.state;
const URL = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${lng}&type=restaurant&radius=${5 *
1000}&key=MY_API_KEY`;
navigator.geolocation.getCurrentPosition(
async position => {
this.setState({ lat: position.coords.latitude });
this.setState({ lng: position.coords.longitude });
const URL = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${
position.coords.latitude
},${position.coords.longitude}&type=restaurant&radius=${5 *
1000}&key=MY_API_KEY`;
const response = await axios.get(URL);
console.log(response.data);
places = response.data.results;
this.setState({ places });
},
error => {
console.log('Error getting location');
}
);
};
async componentDidMount() {
const url = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=
${this.state.lat},${this.state.lng}type=restaurant&radius=${2 *
1000}&key=MY_API_KEYE`;
const response = await fetch(url);
const data = await response.json();
this.setState({ places: data.results });
console.log(data.results);
}
handleScriptLoad = () => {
// Declare Options For Autocomplete
const options = {
types: ['address']
}; // To disable any eslint 'google not defined' errors
// Initialize Google Autocomplete
/*global google*/ this.autocomplete = new google.maps.places.Autocomplete(
document.getElementById('autocomplete'),
options
);
// Avoid paying for data that you don't need by restricting the set of
// place fields that are returned to just the address components and formatted
// address.
this.autocomplete.setFields(['address_components', 'formatted_address']);
// Fire Event when a suggested name is selected
this.autocomplete.addListener('place_changed', this.handlePlaceSelect);
};
handlePlaceSelect = async () => {
let { query, lat, lng } = this.state;
this.setState({ loading: true });
// Extract City From Address Object
const addressObject = this.autocomplete.getPlace();
const address = addressObject.address_components;
Geocode.setApiKey('MY_API_KEY');
// Check if address is valid
let city;
if (address) {
city = address[0].long_name;
query = addressObject.formatted_address;
}
try {
const response = await Geocode.fromAddress(query);
({ lat, lng } = response.results[0].geometry.location);
} catch (error) {
console.error(error);
}
let places;
try {
const URL = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${lat},${lng}&type=restaurant&radius=${5 *
1000}&key=MY_API_KEY`;
const response = await axios.get(URL);
console.log(response.data);
places = response.data.results;
} catch (error) {
console.log(error.message);
}
this.setState({ query, places, city, lat, lng });
setTimeout(() => this.setState({ loading: false }), 400);
};
render() {
const { loading } = this.state;
return (
<div>
<div className="flex align-center">
<div className="search">
<SearchInput
id="autocomplete"
placeholder="Search by address"
width="100%"
height={56}
/>
<Script
url="https://maps.googleapis.com/maps/api/js?key=MY_API_KEY&libraries=places,geometry&callback=initAutocomplete"
onLoad={this.handleScriptLoad}
/>
</div>
<div className="current-location">
<Tooltip content="Use current location">
<IconButton
icon="locate"
iconSize={16}
height={32}
onClick={this.currentLocationOnClick}
>
{this.state.lat} & {this.state.lng}
</IconButton>
</Tooltip>
</div>
</div>
<div className="results">
{this.state.places.map(places => {
const Thumbnail = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${places.photos[0].photo_reference}&key=MY_API_KEY
`;
return (
<div
className="results-flex"
onClick={async () => {
let loading;
let sidebar;
let place_detail;
try {
const URL = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${places.place_id}&fields=name,rating,formatted_phone_number&key=
MY_API_KEY`;
const response = await axios.get(URL);
console.log(response.data);
place_detail = response.data.result;
} catch (error) {
console.log(error.message);
}
this.setState(
{ place_detail, sidebar: true }
// () => this.props.sideBarOpen()
);
}}
>
{this.state.loading ? (
<>
<div className="flex justify-center">
<Placeholder />
</div>
</>
) : (
<FadeIn>
<ResultsItem
name={places.name}
image={Thumbnail}
rating={places.rating}
rating_total={places.user_ratings_total}
/>
</FadeIn>
)}
</div>
);
})}
<SideSheet
isShown={this.state.sidebar}
onCloseComplete={() => this.setState({ sidebar: false })}
>
<Paragraph margin={40}>{this.state.place_detail.name}</Paragraph>
</SideSheet>
</div>
</div>
);
}
}
export default Autocomplete;
I think the best solution is to assign it to a variable :
const photoReference = places && places.photos[0] ? places.photos[0].photo_reference : placeholder;
try using if else for it
for eg:
if(!!places.photos[0]) {
const Thumbnail =
`https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${places.photos[0].photo_reference}&key=MY_API_KEY
}
else { const Thumbnail = `another image url` }

How to setState to answer from APi and use map

Im trying to create recipes searcher. In App.js I receive query from search input from another component and I want to setState to answer from APi. Console.log from callback in setState shows updated state but the state is not updated. I need setState updaed so I can use map on it and display list of recipes in render. It gives me error map is not a function because this.state.recipesList is still empty. Anyone can help me ?
class App extends Component {
state = {
query: "",
recipesList: []
};
getQuery = query => {
const key = "2889f0d3f51281eea62fa6726e16991e";
const URL = `https://www.food2fork.com/api/search?key=${key}&q=${query}`;
fetch(URL)
.then(res => res.json())
.then(res => {
this.setState(
{
recipesList: res
},
() => {
console.log(this.state.recipesList);
}
);
});
console.log(this.state.recipesList);
};
render() {
const test = this.state.recipesList.map(item => {
return (
<div className="recispesList">
<h1>{item.title}</h1>
</div>
);
});
return (
<div className="App">
<Search query={this.getQuery} />
<div className="contentWrapper">{}</div>
</div>
);
}
}
Search component:
class Search extends Component {
state = {
searchValue: ""
};
handleChange = val => {
let searchValue = val.target.value;
this.setState({
searchValue
});
};
handleSubmit = e => {
e.preventDefault();
this.setState({
searchValue: ""
});
this.props.query(this.state.searchValue);
};
render() {
return (
<div className="searchWrapper">
<form onSubmit={this.handleSubmit}>
<input onChange={this.handleChange} value={this.state.searchValue} />
<button />
</form>
</div>
);
}
}
export default Search;
It seems that instead of directly assigning the whole response to recipesList:
this.setState(
{
recipesList: res
},
() => {
console.log(this.state.recipesList);
}
);
you need to get recipes array first via res.recipes:
this.setState(
{
recipesList: res.recipes
},
() => {
console.log(this.state.recipesList);
}
);

How do I manage my array of children components' states?

I'm new to react, so forgive me. I'm having a problem understanding states, specifically those of children.
Purpose: I'm trying to create a form that a user can append more and more components -- in this case, images.
What happens: User appends 2 or more images. User tries to upload an image with UploadButton component, but both the images are the same. I believe this has to do with both appended children sharing the same state.
Question: How do I give each appended child its own image without affecting the other appended children?
class Page extends Component
constructor (props) {
super(props);
this.state = {
id: '',
numChildren: 0,
images: [],
}
this.onAddChild = this.onAddChild.bind(this);
}
showModal() {
this.setState({
numChildren: 0,
images: [],
});
}
renderModal()
const children = [];
//Here's my array of child components
for(var i = 0; i < this.state.numChildren; i += 1) {
children.push(<this.ChildComponent key={i} />);
}
return (
<ReactModal>
<this.ParentComponent addChild={this.onAddChild}>
{children}
</this.ParentComponent>
</ReactModal>
)
}
onAddChild = () => {
this.setState({
numChildren: this.state.numChildren + 1
})
}
ParentComponent = (props) => (
<div>
{props.children}
<Button onClick={props.addChild}>Add Item</Button>
</div>
);
ChildComponent = () => (
<div>
<UploadButton
storage="menus"
value={this.state.images}
onUploadComplete={uri => this.setState({images: uri})}
/>
</div>
);
}
Here's the code for UploadButton:
import React, { Component } from 'react';
import uuid from 'uuid';
import firebase from '../config/firebase';
class UploadButton extends Component {
constructor(props) {
super(props);
this.state = {
isUploading: false
}
}
handleClick() {
const input = document.createElement("INPUT");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/gif, image/jpeg, image/png");
input.addEventListener("change", ({target: {files: [file]}}) => this.uploadFile(file));
input.click();
}
uploadFile(file) {
console.log('F', file);
const id = uuid.v4();
this.setState({ isUploading: true })
const metadata = {
contentType: file.type
};
firebase.storage()
.ref('friends')
.child(id)
.put(file, metadata)
.then(({ downloadURL }) => {
this.setState({ isUploading: false })
console.log('Uploaded', downloadURL);
this.props.onUploadComplete(downloadURL);
})
.catch(e => this.setState({ isUploading: false }));
}
render() {
const {
props: {
value,
style = {},
className = "image-upload-button",
},
state: {
isUploading
}
} = this;
return (
<div
onClick={() => this.handleClick()}
className={className}
style={{
...style,
backgroundImage: `url("${this.props.value}")`,
}}>
{isUploading ? "UPLOADING..." : !value ? 'No image' : ''}
</div>
);
}
}
export default UploadButton;
I tried to exclude all unnecessary code not pertaining to my problem, but please, let me know if I need to show more.
EDIT: This is my attempt, it doesn't work:
//altered my children array to include a new prop
renderModal() {
const children = [];
for (var i = 0; i < this.state.numChildren; i += 1) {
children.push(<this.ChildComponent imageSelect={this.onImageSelect} key={i} />);
}
//...
};
//my attempt to assign value and pass selected image back to images array
ChildComponent = () => (
<div>
<UploadButton
storage="menus"
value={uri => this.props.onImageSelect(uri)} //my greenness is really apparent here
onUploadComplete={uri => this.setState({images: uri})}
/>
//...
</div>
);
//added this function to the class
onImageSelect(uri) {
var el = this.state.images.concat(uri);
this.setState({
images: el
})
}
I know I'm not accessing the child prop correctly. This is the most complexity I've dealt with so far. Thanks for your time.
When you write this.state in Child / Parent component, you are actually accessing the state of Page. Now, I would recommend that you pass in the index of the child to the Child like so
children.push(<this.ChildComponent key={i} index={i}/>)
so that each children deals with only its own image like so
ChildComponent = ({index}) => (
<div>
<UploadButton
storage="menus"
value={this.state.images[index]}
onUploadComplete={uri => {
let images = this.state.images.slice()
images[index] = uri
this.setState({images})
}}
/>
</div>
);

Resources