In today post we will use usetheform + draftjs (Rich Text Editor Framework for React) to build an example of a Twitter what's happening form bar.
🎉 Final Result 🎉
Introduction
When we post a new Tweet it basically consists of a collection of information like images, videos, plain text, emoji, ...etc which will be sent to Twitter through a POST api.
If we wanted to build something similar to its what's happening bar, we could think to develop it as a web Form.
A web form consists of a collection of HTML field elements grouped within a <form> tag. HTML supports different field elements, like for instance:
- <input /> which defines an HTML form for user input
- <textarea /> which defines a multi-line input control (text area)
- <select /> which defines a drop-down list
For the complete list of all the HTML form elements supported, please refer to w3schools form elements.
Setting up the Form Skeleton
In React, we can declaratively reproduce our form skeleton like the following:
import React from "react";
import { Form } from "usetheform";
// ...rest of the imports
const MAX_CHARS_ALLOWED = 50;
export default function App() {
return (
<Form onSubmit={(formState) => console.log(formState) }>
<WhatsHappeningBar maxChars={MAX_CHARS_ALLOWED} />
<Preview />
<PrivacyPicker />
<UploadMediaBar />
<CharacterCounter maxChars={MAX_CHARS_ALLOWED} />
<Submit />
</Form>
);
}
When an user submits the form, the value of formState will look like:
const formState= {
editor: {
editorState: {}, // the Draftjs editor state
refEditor: {}, // a DOM ref to the Draftjs editor
plainText: "abc ...etc"
},
postPrivacy: "0", // possible values "0","1","2"
gif: { }, // present if any gif is uploaded,
media: [img, video, ..etc] // present if any media is uploaded
}
To better understand how the above formState is composed we will focus on the components that create and handle the main "pieces" of the formState.
Let's start
The first important "piece" of the formState we are going to analyze is the editor:
const formState= {
....,
editor: {
editorState: {}, // the Draftjs editor state
refEditor: {}, // a DOM ref to the Draftjs editor
plainText: "abc ...etc"
}
}
which is created and handled by the <WhatsHappeningBar /> component.
⚛️ WhatsHappeningBar
import React from "react";
import { Collection, Input } from "usetheform";
import { DraftEditor } from "./DraftEditor";
import { extractPlainText } from "./utils/extractPlainText";
import { limitTo } from "./utils/limitTo";
export const WhatsHappeningBar = ({ maxChars }) => {
return (
<Collection
object
name="editor"
validators={[limitTo(maxChars)]}
reducers={extractPlainText}
>
<DraftEditor name="editorState" maxChars={maxChars} />
<Input type="hidden" name="plainText" />
</Collection>
);
};
An object or an array of "usetheform" library is represented by the <Collection /> component, which creates within the form state the editor object.
Collection component contains the validation function which validates the form state based on the text length of the editor and also a reducer function which extracts the "plainText" from the draftjs "editorState". The full code at: validator and reducer
For more details on how <Collection /> works, please refer to the Collection docs.
The second "piece" within the formState we will look at is the postPrivacy
const formState= {
....,
postPrivacy: "0", // possible values "0","1","2"
}
which is created and handled by the <PrivacyPicker /> component.
⚛️ PrivacyPicker
For sake of simplicity, what is shown below is a basic implementation of the <PrivacyPicker /> component made up be three input radios. Full implementation at: PrivacyPicker component
import React from "react";
import { Input} from "usetheform";
export const PrivacyPicker = () => {
return (
<div className="PrivacySelection__Radios">
<Input name="postPrivacy" type="radio" value="0" checked />
<Input name="postPrivacy" type="radio" value="1" />
<Input name="postPrivacy" type="radio" value="2" />
</div>
);
};
The <Input type="radio" /> component of "usetheform" creates a piece of state within a Form named "postPrivacy", which holds the privacy value picked by the user. More details about it at: Input docs.
Another component worthy to be mentioned is the <UploadGif />
which creates and handles the following "piece" of formState:
const formState= {
....,
gif: { ...gifProps }
}
⚛️ UploadGif
import React, { useEffect, useState } from "react";
import { useField } from "usetheform";
export const UploadGif = () => {
const { setValue } = useField({ type: "custom", name: "gif" });
const [showGrid, toggleGrid] = useState(() => false);
const toggleGifGrid = () => toggleGrid((prev) => !prev);
const onGifClick = (gif, e) => {
e.preventDefault();
setValue(gif);
toggleGifGrid();
};
return (
<div>
<button type="button" onClick={toggleGifGrid}>
<img alt="Upload GIF" src={UpladGifSVG} />
</button>
{showGrid && (
<GifGrid onCloseGifGrid={toggleGifGrid} onGifClick={onGifClick} />
)}
</div>
);
};
The useField hook allows to build a custom input primitives.
When a user picks any gif image a callback function will be invoked and the gif object will be pushed into the formState:
const onGifClick = (gif, e) => {
e.preventDefault();
setValue(gif); // pushing into the formState
toggleGifGrid();
};
More details about useField at: useField doc
The last, but not least, component we will look at is the <CharacterCounter/> component.
⚛️ CharacterCounter
import React from "react";
import { useSelector } from "usetheform";
import { ProgressRingBar } from "./../ProgressRingBar/ProgressRingBar";
import { getProgressRingBarProps } from "./utils/getProgressRingBarProps";
export const CharacterCounter = ({ maxChars }) => {
const [plainText] = useSelector((state) => state.editor.plainText);
const props = getProgressRingBarProps(plainText, maxChars);
return (
<ProgressRingBar {...props} />
);
};
The CharacterCounter component counts the typed characters. In order to do so it uses the useSelector hook for picking the plainText from the form state using the utils function named getProgressRingBarProps .
More details about useSelector at: useSelector doc
Conclusion
Hope you enjoyed reading this post. If you did so, please, use the buttons below to share it. 🙏 Thanks for reading!
Top comments (0)