Next.js - How to Get Session Information in Server Side vs Client Side iusing Next-auth
Introduction Next-auth is a fantastic library for abstracting handling of…
May 07, 2021
In this post, we will use Draft.js WYSWYG editor
in Next.js with strapi. And, we will create a write article page
.
We will do following:
Previously we have seen:
In this post, I will be using my Next.js starter bootstrap
Draft.js as an awesome WYSWYG editor from facebook. It is one of the top used component in React. Lets integrate it into our Next.js application.
Modules required
draft-js
react-draft-wysiwyg
Its pertty simple to use. And you can customize it according to your need. I’m using the simple default one in this app.
Lets create a separate component for Draft.js, so that we can easily customize it later on.
Add a new file at: /components/editor/editor.jsx
I will be using my Utility class for REST API in Next.js
import React, { Component } from 'react'
import {EditorState} from "draft-js";
import dynamic from 'next/dynamic';
import apiClient from '../api/api_client'
import { convertFromRaw, convertToRaw } from 'draft-js';
const Editor = dynamic(
() => import('react-draft-wysiwyg').then(mod => mod.Editor),
{ ssr: false }
)
import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
export default class ArticleEditor extends Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty()
};
}
onEditorStateChange = (editorState) => {
this.setState({
editorState,
});
this.props.handleContent(
convertToRaw(editorState.getCurrentContent()
));
};
uploadImageCallBack = async (file) => {
const imgData = await apiClient.uploadInlineImageForArticle(file);
return Promise.resolve({ data: {
link: `${process.env.NEXT_PUBLIC_API_URL}${imgData[0].formats.small.url}`
}});
}
render() {
const { editorState } = this.state;
return (
<Editor
editorState={editorState}
toolbarClassName="toolbar-class"
wrapperClassName="wrapper-class"
editorClassName="editor-class"
onEditorStateChange={this.onEditorStateChange}
// toolbarOnFocus
toolbar={{
options: ['inline', 'blockType', 'fontSize', 'fontFamily', 'list', 'textAlign', 'colorPicker', 'link', 'embedded', 'emoji', 'image', 'history'],
inline: { inDropdown: true },
list: { inDropdown: true },
textAlign: { inDropdown: true },
link: { inDropdown: true },
history: { inDropdown: true },
image: {
urlEnabled: true,
uploadEnabled: true,
uploadCallback: this.uploadImageCallBack,
previewImage: true,
alt: { present: false, mandatory: false }
},
}}
/>
)
}
}
First notice, how we have used: react-draft-wysiwyg
. It has been imported differently.
If we import it like:
import Editor from 'react-draft-wysiwyg';
Then, we will get following error:
Server Error
ReferenceError: window is not defined
We have set two event handlers for title and body.
For the body where we have used Draft.js WYSWYG
. We are using a Draft.js function convertToRaw
to save data in state.
We have configured an option in options for Draft.js
image: {
urlEnabled: true,
uploadEnabled: true,
uploadCallback: this.uploadImageCallBack,
previewImage: true,
alt: { present: false, mandatory: false }
},
By default, We have enabled image upload. And have defined a callback handler for image upload. I have made alt off for users to enter. Else, the dialog box for image upload will not allow to upload image without entering alt attribute for image. Although, its a good feature but think from a user’s perspective. Why would he be forced to enter this.
For Image upload, I have used my Rest client class.
uploadImageCallBack = async (file) => {
const imgData = await apiClient.uploadInlineImageForArticle(file);
return Promise.resolve({ data: {
link: `${process.env.NEXT_PUBLIC_API_URL}${imgData[0].formats.small.url}`
}});
}
The upload image callback need to return a promise with link of image. So that, it can show that image in the editor. In your case, the path might change.
And, the code from apiClient for uploading image:
async uploadInlineImageForArticle(file) {
const headers = await this.getAuthHeader();
const formData = new FormData();
formData.append('files', file);
try {
let { data } = await axios.post(
`${process.env.NEXT_PUBLIC_API_URL}/upload`,
formData,
{
headers: headers,
}
)
return data;
} catch (e) {
console.log('caught error');
console.error(e);
return null;
}
}
Note, you would need to configure permission to upload image in your strapi backend.
Page: /pages/write.jsx
import SimpleLayout from '../components/layout/simple'
import React, { Component } from 'react'
import { Form, Button, Card } from 'react-bootstrap';
import { withRouter } from 'next/router'
import ArticleEditor from '../components/editor/editor'
import apiClient from '../components/api/api_client'
class Write extends Component {
constructor(props) {
super(props);
this.state = {
title: "",
body: ""
};
this.handleInputs = this.handleInputs.bind(this);
this.submitForm = this.submitForm.bind(this);
}
handleInputs = (event) => {
let {name, value} = event.target
this.setState({
[name]: value
});
}
handleEditorContent = (content) => {
this.setState({
body: content,
articleUpdated: true
});
}
submitForm = async (event) => {
event.preventDefault()
let article = await apiClient.saveArticle({
title: this.state.title,
body: JSON.stringify(this.state.body)
})
this.props.router.push(`/articles/${article.slug}`);
}
render() {
return (
<SimpleLayout>
<div className="row">
<div className="col-8">
<Form onSubmit={this.submitForm}>
<Form.Group controlId="formBasicEmail">
<Form.Label>Headline</Form.Label>
<Form.Control type="text"
name="title"
value={this.state.title}
onChange={this.handleInputs} />
<Form.Text className="text-muted">
Give a nice title to your article
</Form.Text>
</Form.Group>
<Form.Group controlId="exampleForm.ControlTextarea2">
<Form.Label>Body</Form.Label>
<Card className="p-2">
<ArticleEditor
handleContent={this.handleEditorContent}
/>
</Card>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</div>
<div className="col-4">
Another col
</div>
</div>
</SimpleLayout>
)
}
}
export default withRouter(Write);
So we have integrated our Draft.js component here. We are setting the state as blank title and body.
I hope you have read Configure strapi for sluify url
When we submit request to save an article. We get the data back and a slug field as well. We would want the URL to be something like:
/articles/article-title-slug
Example, I have put title as: Hello Article 1
, following is the response:
author: {confirmed: true, blocked: false, _id: "608e0771a81d84396e94a1d8", username: "test", email: "[email protected]", …}
body: "{"blocks":[{"key":"dcdja","text":"Hi Body","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"8fnnj","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"7lcfr","text":" ","type":"atomic","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":0,"length":1,"key":0}],"data":{}},{"key":"7dk6o","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{"0":{"type":"IMAGE","mutability":"MUTABLE","data":{"src":"http://localhost:1337/uploads/small_cricket_155965_640_ab625e04e3.png","height":"auto","width":"auto"}}}}"
createdAt: "2021-05-07T10:47:37.411Z"
id: "60951ac97425296c83caf0fe"
published_at: "2021-05-07T10:47:37.392Z"
slug: "hello-article-1"
title: "Hello Article 1"
updatedAt: "2021-05-07T10:47:37.418Z"
__v: 0
_id: "60951ac97425296c83caf0fe"
Note the slug: slug: "hello-article-1"
To redirect, I have imported withRouter
import { withRouter } from 'next/router'
And, exported the component wrapped under withRouter
export default withRouter(Write);
Now, in submitForm
handler, I have used below code:
this.props.router.push(`/articles/${article.slug}`);
Now, I should be having a page which will receive this call:
/pages/articles/[...slug].jsx
import SimpleLayout from '../../components/layout/simple'
import draftToHtml from 'draftjs-to-html';
import { useSession, getSession } from 'next-auth/client'
import React from 'react'
import Link from 'next/link'
import ErrorPage from 'next/error'
import apiClient from '../../components/api/api_client'
export default function Article(props) {
if (props.error) {
return <SimpleLayout>
<ErrorPage statusCode={props.error.statusCode} />
</SimpleLayout>
}
const body = draftToHtml(JSON.parse(props.body));
return (
<SimpleLayout>
<h1>{props.title}</h1>
<h4>Author: {props.author.username}</h4>
<div dangerouslySetInnerHTML={{__html: body}}></div>
</SimpleLayout>
)
}
export async function getServerSideProps(context) {
const session = await getSession(context);
try {
let data = await apiClient.getArticleBySlug(context.query.slug[0]);
if (!data || data.length == 0) {
return {props: {error: {statusCode: 404}}}
}
return {props: data[0]}
} catch(error) {
return {props: {error: {statusCode: 404}}}
}
}
Here, I’m fetching article by slug by api:
GET /articles?slug=<slug>
Page will look like:
Note the url: http://localhost:3000/articles/hello-article-1
We have two workflows:
Whatever text and image we have written in the WYSWYG editor, it will not be saved as text. It generates a json.
If you notice above code, on editor content change, we are saving state as:
convertToRaw(editorState.getCurrentContent()
And, in write.jsx
, before saving, we are doing:
body: JSON.stringify(this.state.body)
A sample json from above example:
{
"blocks": [
{
"key": "dcdja",
"text": "Hi Body",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
},
{
"key": "8fnnj",
"text": "",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
},
{
"key": "7lcfr",
"text": " ",
"type": "atomic",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [
{
"offset": 0,
"length": 1,
"key": 0
}
],
"data": {}
},
{
"key": "7dk6o",
"text": "",
"type": "unstyled",
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [],
"data": {}
}
],
"entityMap": {
"0": {
"type": "IMAGE",
"mutability": "MUTABLE",
"data": {
"src": "http://localhost:1337/uploads/small_cricket_155965_640_ab625e04e3.png",
"height": "auto",
"width": "auto"
}
}
}
}
And we save this json as string in our database.
Now, its time to load the content.
We are using a module draftjs-to-html
and below code:
import draftToHtml from 'draftjs-to-html';
const body = draftToHtml(JSON.parse(props.body));
<div dangerouslySetInnerHTML={{__html: body}}></div>
draftToHtml
convert our draft.json to renderable html. Which we are using in last div tag.
Next.js provides a handler for this. We have used following code:
import ErrorPage from 'next/error'
<ErrorPage statusCode={props.error.statusCode} />
From getServerSideProps
, we are checking if we got nothing by get api. And, then setting error code.
In the function component, we are checking if error is set, redirect to that error code page.
This article is already so long, I’m moving Edit part in nexrt article: Edit Article in Next.js with Draft.js
In next post, we will see complete code in Github.
Introduction Next-auth is a fantastic library for abstracting handling of…
Introduction In this post, we will see how we can create a content type. And…
Introduction In this post we will see: How to prepare a docker image for your…
Note: This is based on bootstrap-4 If you are using multi column design in your…
Introduction In your backend and frontend projects, you always need to deal with…
Introduction We have a page, for example a . And, upon submission we would want…
Introduction This post has the complete code to send email through smtp server…
Introduction In a normal email sending code from python, I’m getting following…
Introduction In one of my app, I was using to talk to . I have used some event…
Introduction So you have a Django project, and want to run it using docker image…
Introduction It is very important to introduce few process so that your code and…
Introduction In this post, we will see a sample Jenkin Pipeline Groovy script…