30
loading...
This website collects cookies to deliver better user experience
export default function CreateForm() {
// Using react-hook-form to handle data-binding with form fields.
// With it you can prepopulate form fields, set error messages per field etc.
const form = useForm()
// state variables to store error messages and a loading flag
const [error, setError] = React.useState('')
const [loading, setLoading] = React.useState(false)
// rendering the page
return (
<React.Fragment>
<h4>Create Post</h4>
<form onSubmit={form.handleSubmit(onCreate)}>
<div>
<label>Title</label>
<input type='text' {...form.register("title")}></input>
</div>
<div>
<label>Body</label>
<textarea {...form.register("body")}></textarea>
</div>
{error ? <p>{error}</p> : null}
<input
type='submit'
disabled={loading}
value={loading ? 'Please Wait ...' : 'Submit'}>
</input>
</form>
</React.Fragment>
)
}
The presentation layer handles the rendering of the page, hooking up the DOM events with the BLOC inputs, and re-rendering when triggered by BLOC outputs.
The BLOC layer handles all the reactive logic and API requests.
/*
* CreateFormBloc.js (BLOC layer)
*/
import { Subject, mapTo, merge, of, startWith, switchMap, share, filter } from "rxjs";
import { fromFetch } from 'rxjs/fetch';
const BASE_URL = 'https://jsonplaceholder.typicode.com'
export default class CreateFormBloc {
constructor() {
this.formData = new Subject(); // Subjects are used to accept inputs to the BLOC
this.createPost = this.formData.pipe(
switchMap(data => fromFetch(`${BASE_URL}/posts`, { method: 'POST', body: JSON.stringify(data) })),
switchMap(resp => {
if (resp.ok) {
return resp.json()
} else {
return of(new Error('Error Occurred when creating post'))
}
}),
share() // share() operator prevents the API from triggering multiple times on each downward streams.
)
// the SUCCESS output stream. React.js can subscribe to this and render a success message.
this.createPostSuccess = this.createPost.pipe(
filter(resp => !(resp instanceof Error))
)
// the FAILED output stream. React.js can subscribe to this and render an error message.
this.createPostFailed = this.createPost.pipe(
filter(resp => resp instanceof Error)
)
// Emits a boolean flag indicating whether submission is in progress or not.
this.createPostInProgress = merge(
this.formData.pipe(mapTo(true)),
this.createPost.pipe(mapTo(false)),
).pipe(
startWith(false),
)
}
}
/*
* CreateForm.js (Presentation Layer)
*/
export default function CreateForm() {
const [bloc] = React.useState(new CreateFormBloc())
const form = useForm()
const [error, setError] = React.useState('')
const [loading, setLoading] = React.useState(false)
React.useEffect(() => {
/*
* subscribing to BLOC output streams, triggering the page to re-render.
*/
const sub = new Subscription()
sub.add(bloc.createPostSuccess.subscribe(_ => alert('Post Created Successfully!')))
sub.add(bloc.createPostFailed.subscribe(err => setError(err.message)))
sub.add(bloc.createPostInProgress.subscribe(setLoading))
return () => sub.unsubscribe() // unsubscribe the event handlers when component is destroyed.
}, [])
// when form submits, we input the form data into the BLOC
function onCreate(data) {
bloc.formData.next(data)
}
return (
<form onSubmit={form.handleSubmit(onCreate)}>
// .... rendering logic
)
}
/*
* EditFormBloc.js (BLOC layer)
*/
import { of, Subject, switchMap, withLatestFrom, share, filter, merge, mapTo, startWith } from "rxjs";
import { fromFetch } from 'rxjs/fetch';
const BASE_URL = 'https://jsonplaceholder.typicode.com'
export default class EditFormBloc {
constructor() {
this.formData = new Subject()
// Subject to input the ID of the Post object being edited
this.postID = new Subject()
// When postID is inputted, BLOC will fetch the Post object.
// React.js can use this to pre-populate the form fields.
this.initialFormData = this.postID.pipe(
switchMap(postID => fromFetch(`${BASE_URL}/posts/${postID}`)),
switchMap(resp => resp.json()),
)
// updating the Post object when form is submitted
this.updatePost = this.formData.pipe(
withLatestFrom(this.postID),
switchMap(([data, postID]) => {
const url = `${BASE_URL}/posts/${postID}`
const payload = { method: 'PUT', body: JSON.stringify(data) }
return fromFetch(url, payload)
}),
switchMap(resp => {
if (resp.ok) {
return resp.json()
} else {
return of(new Error('Error updating Post'))
}
}),
share(),
)
// BLOC output. React.js will subscribe and display a success message.
this.updatePostSuccess = this.updatePost.pipe(
filter(resp => !(resp instanceof Error))
)
// BLOC output. React.js will subscribe and display an error message.
this.updatePostFailed = this.updatePost.pipe(
filter(resp => resp instanceof Error)
)
// BLOC output. React.js will subscribe and disable the submit button accordingly.
this.updatePostInProgress = merge(
this.formData.pipe(mapTo(true)),
this.updatePost.pipe(mapTo(false)),
).pipe(
startWith(false),
)
}
}
/*
* EditForm.js (Presentation Layer)
*/
import React from 'react'
import { useForm } from 'react-hook-form'
import { Subscription } from 'rxjs'
import EditFormBloc from './EditFormBloc'
import { useRouteMatch } from 'react-router-dom'
export default function EditForm() {
const form = useForm()
const match = useRouteMatch()
const [error, setError] = React.useState('')
const [loading, setLoading] = React.useState(false)
const [bloc] = React.useState(new EditFormBloc())
React.useEffect(() => {
const sub = new Subscription()
/*
* Subscribe to BLOC output streams.
* So we can display when submission is successful/failed/in progress
* We also subscribe to the initialFormData stream, and pre-populate the form fields.
*/
sub.add(bloc.updatePostSuccess.subscribe(_ => alert('Post Updated Successfully!')))
sub.add(bloc.updatePostFailed.subscribe(err => setError(err.message)))
sub.add(bloc.updatePostInProgress.subscribe(setLoading))
sub.add(bloc.initialFormData.subscribe(data => {
form.setValue('title', data.title, { shouldValidate: true, shouldDirty: false })
form.setValue('body', data.body, { shouldValidate: true, shouldDirty: false })
}))
return () => sub.unsubscribe() // unsubscribe the event handlers when component is destroyed.
}, [])
React.useEffect(() => {
// When the page loads, we get the Post ID from URL parameter and input into the BLOC
bloc.postID.next(match.params.post_id)
}, [])
// When form submits, we input formData into the BLOC to trigger API call.
function onUpdate(data) {
bloc.formData.next(data)
}
return (
<form onSubmit={form.handleSubmit(onUpdate)}>
// ... rendering logic
)
}
/*
* PostForm.js
*/
import React from 'react'
export default function PostForm(props) {
const { form, error, loading, onSubmit } = props
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<label>Title</label>
<input type='text' {...form.register("title")}></input>
</div>
<div>
<label>Body</label>
<textarea {...form.register("body")}></textarea>
</div>
{error ? <p>{error}</p> : null}
<input
type='submit'
disabled={loading}
value={loading ? 'Please Wait ...' : 'Submit'}>
</input>
</form>
)
}
/*
* CreateForm.js
*/
export default function CreateForm() {
const form = useForm()
const [error, setError] = React.useState('')
const [loading, setLoading] = React.useState(false)
// ...
return (
<React.Fragment>
<h4>Create Post</h4>
<PostForm
form={form}
error={error}
loading={loading}
onSubmit={onCreate}>
</PostForm>
</React.Fragment>
)
}
/*
* EditForm.js
*/
export default function EditForm() {
const form = useForm()
const [error, setError] = React.useState('')
const [loading, setLoading] = React.useState(false)
// ...
return (
<React.Fragment>
<h4>Edit Post</h4>
<PostForm
form={form}
error={error}
loading={loading}
onSubmit={onUpdate}>
</PostForm>
</React.Fragment>
)
}
/*
* FetchPostMixin.js
*/
import { Mixin } from 'mixwith'
import { has } from "lodash";
import { of, Subject, switchMap } from "rxjs";
import { fromFetch } from 'rxjs/fetch';
const BASE_URL = 'https://jsonplaceholder.typicode.com'
let FetchPostMixin = Mixin((superclass) => class extends superclass {
get postID() {
if (!has(this, '_postID')) {
this._postID = new Subject()
}
return this._postID
}
get post() {
if (!has(this, '_post')) {
this._post = this.postID.pipe(
switchMap(postID => fromFetch(`${BASE_URL}/posts/${postID}`)),
switchMap(resp => {
if (resp.ok) {
return resp.json()
} else {
return of(new Error('Error fetching Post'))
}
}),
)
}
return this._post
}
});
export default FetchPostMixin
/*
* ViewPageBloc.js (BLOC layer)
*/
import { mix } from "mixwith";
import FetchPostMixin from "blocs/FetchPostMixin";
export default class ViewPostBloc extends mix(Object).with(FetchPostMixin) { }
/*
* ViewPage.js (Presentation layer)
*/
import React from 'react'
import { useRouteMatch } from 'react-router-dom'
import { Subscription } from 'rxjs'
import ViewPostBloc from 'blocs/ViewPostBloc'
export default function ViewPost() {
const match = useRouteMatch()
const [bloc] = React.useState(new ViewPostBloc())
const [post, setPost] = React.useState()
React.useEffect(() => {
const sub = new Subscription()
sub.add(bloc.post.subscribe(setPost))
return () => sub.unsubscribe()
}, [])
React.useEffect(() => {
bloc.postID.next(match.params.post_id)
}, [])
return (
<React.Fragment>
<h4>View Post</h4>
{post ? (
<dl>
<dt>Title</dt>
<dd>{ post.title }</dd>
<dt>Body</dt>
<dd>{ post.body }</dd>
</dl>
) : (
<p>Please Wait ...</p>
)}
</React.Fragment>
)
}
/*
* EditFormBloc.js
*/
import { mix } from "mixwith";
import FetchPostMixin from "blocs/FetchPostMixin";
const BASE_URL = 'https://jsonplaceholder.typicode.com'
export default class EditFormBloc extends mix(Object).with(FetchPostMixin) {
get formData() {
// ...
}
get updatePost() {
// ...
}
get updatePostSuccess() {
// ...
}
get updatePostFailed() {
// ...
}
get updatePostInProgress() {
// ...
}
}
/*
* EditForm.js
*/
import React from 'react'
import { useForm } from 'react-hook-form'
import { Subscription } from 'rxjs'
import PostForm from 'components/PostForm'
import EditFormBloc from 'blocs/EditFormBloc'
import { useRouteMatch } from 'react-router-dom'
export default function EditForm() {
const form = useForm()
const match = useRouteMatch()
const [error, setError] = React.useState('')
const [loading, setLoading] = React.useState(false)
const [bloc] = React.useState(new EditFormBloc())
React.useEffect(() => {
const sub = new Subscription()
sub.add(bloc.updatePostSuccess.subscribe(_ => alert('Post Updated Successfully!')))
sub.add(bloc.updatePostFailed.subscribe(err => setError(err.message)))
sub.add(bloc.updatePostInProgress.subscribe(setLoading))
sub.add(bloc.post.subscribe(post => {
form.setValue('title', post.title, { shouldValidate: true, shouldDirty: false })
form.setValue('body', post.body, { shouldValidate: true, shouldDirty: false })
}))
return () => sub.unsubscribe()
}, [])
React.useEffect(() => {
bloc.postID.next(match.params.post_id)
}, [])
function onUpdate(data) {
bloc.formData.next(data)
}
return (
// ... rendering logic
)
}