Skip to content

Instantly share code, notes, and snippets.

@calderaro
Last active May 21, 2020 07:32
Show Gist options
  • Save calderaro/5383166bfb7c7b0c96f97014055e0731 to your computer and use it in GitHub Desktop.
Save calderaro/5383166bfb7c7b0c96f97014055e0731 to your computer and use it in GitHub Desktop.
Async Handlers
import React from "react";
export interface FormHandlerChildrenProps<T> {
state: FormState<T>;
reload?: () => Promise<void>;
create?: () => Promise<void>;
update?: () => Promise<void>;
remove?: () => void;
onChange: (change: { id: string; value: any }) => void;
setModal: (name: string) => void;
setSection: (name: string) => void;
}
export interface FormHandlerProps<T> {
id?: string;
onCreateSuccess?: (data: T) => void;
onUpdateSuccess?: (data: T) => void;
onRemoveSuccess?: (id: string) => void;
getDefaultState: () => T;
children(res: FormHandlerChildrenProps<T>): React.ReactNode;
load?: (id: string) => Promise<T>;
create?: (data: T) => Promise<T>;
update?: (data: T) => Promise<T>;
remove?: (id: string) => void;
}
export interface FormState<T> {
status: string;
message: string;
modal: string;
section: string;
data: T;
}
class FormHandler<T> extends React.Component<FormHandlerProps<T>, FormState<T>> {
constructor(props: FormHandlerProps<T>) {
super(props);
this.state = {
status: "",
message: "",
modal: "",
section: "",
data: this.props.getDefaultState()
};
}
componentDidMount() {
this.load();
}
setModal = (name: string) => {
this.setState({ modal: name });
};
setSection = (name: string) => {
this.setState({ section: name });
};
load = async () => {
if (this.props.load) {
if (!this.props.id) {
return this.setState({ status: "loaded", data: this.props.getDefaultState(), message: "" });
}
try {
this.setState({ status: "loading", message: "" });
const data = await this.props.load(this.props.id);
this.setState({ status: "loaded", data, message: "" });
} catch (error) {
if (error.code === "permission-denied") {
return this.setState({ status: "failure", message: "Permisos insuficientes!" });
}
this.setState({ status: "failure", message: "Un error inesperado ha ocurrido!" });
}
}
};
create = async () => {
if (this.props.create) {
try {
this.setState({ status: "creating", message: "" });
const data = await this.props.create(this.state.data);
this.props.onCreateSuccess?.(data);
this.setState({ status: "created", data, message: "Guardado exitoso!" });
} catch (error) {
if (error.code === "permission-denied") {
return this.setState({ status: "failure", message: "Permisos insuficientes!" });
}
this.setState({ status: "failure", message: "Un error inesperado ha ocurrido!" });
}
}
};
update = async () => {
if (this.props.update && this.props.id) {
try {
this.setState({ status: "updating", message: "" });
const data = await this.props.update(this.state.data);
this.props.onUpdateSuccess?.(data);
this.setState({ status: "updated", data, message: "Guardado exitoso!" });
} catch (error) {
if (error.code === "permission-denied") {
return this.setState({ status: "failure", message: "Permisos insuficientes!" });
}
this.setState({ status: "failure", message: "Un error inesperado ha ocurrido!" });
}
}
};
remove = async () => {
if (this.props.remove && this.props.id) {
try {
this.setState({ status: "deleting" });
await this.props.remove(this.props.id);
this.setState({ status: "removed" });
this.props.onRemoveSuccess?.(this.props.id);
} catch (error) {
if (error.code === "permission-denied") {
return this.setState({ status: "failure", message: "Permisos insuficientes!" });
}
this.setState({ status: "failure", message: "Un error inesperado ha ocurrido!" });
}
}
};
onChange = ({ id, value }: any) => {
this.setState((state) => ({
data: {
...state.data,
[id]: value
}
}));
};
render() {
return this.props.children?.({
state: this.state,
onChange: this.onChange,
setModal: this.setModal,
setSection: this.setSection,
reload: this.props.load ? this.load : undefined,
remove: this.props.remove ? this.remove : undefined,
create: this.props.create ? this.create : undefined,
update: this.props.update ? this.update : undefined
});
}
}
export default FormHandler;
import React from "react";
import debounce from "lodash/debounce";
export interface SearchResult<T> {
hits: T[];
page: number;
pages: number;
}
type OnChangeParams<Y> = <K extends keyof Y>(s: K, a: Y[K]) => void;
export interface ListHandlerChildrenProps<T, Y> {
state: ListState<T, Y>;
reload: () => Promise<void>;
onChangeSearchText: (searchText: string) => void;
onChangeParams: OnChangeParams<Y>;
onNextPage: () => void;
onPrevPage: () => void;
}
export type ListHandlerListFunction<T> = (searchText: string, params?: { page: number }) => Promise<SearchResult<T>>;
export interface ListHandlerProps<T, Y> {
list: ListHandlerListFunction<T>;
getParamsDefaultState?: () => Y;
children(res: ListHandlerChildrenProps<T, Y>): React.ReactNode;
}
export interface ListState<T, Y> {
status: "loading" | "loaded" | "searching" | "searched" | "failure";
searchText: string;
page: number;
params: Y | {};
data: SearchResult<T>;
}
class ListHandler<T, Y> extends React.Component<ListHandlerProps<T, Y>, ListState<T, Y>> {
dsearch: () => Promise<void>;
constructor(props: ListHandlerProps<T, Y>) {
super(props);
this.state = {
status: "loading",
searchText: "",
page: 1,
params: this.props.getParamsDefaultState?.() || {},
data: {
hits: [],
page: 0,
pages: 1
}
};
this.dsearch = debounce(this.search, 220);
}
componentDidMount() {
this.list();
}
componentDidUpdate(prevProps: ListHandlerProps<T, Y>, prevState: ListState<T, Y>) {
if (this.state.searchText !== prevState.searchText) {
this.dsearch();
}
if (this.state.page !== prevState.page) {
this.dsearch();
}
}
list = async () => {
this.setState({ status: "loading" });
const data = await this.props.list(this.state.searchText, { page: this.state.page });
this.setState({ status: "loaded", data });
};
search = async () => {
this.setState({ status: "searching" });
const data = await this.props.list(this.state.searchText, { page: this.state.page });
this.setState({ status: "searched", data });
};
onChangeSearchText = (searchText: string) => {
this.setState({ searchText });
};
onChangeParams: OnChangeParams<Y> = (field, value) => {
this.setState({ params: { ...this.state.params, [field]: value } });
};
onNextPage = () => {
if (this.state.page + 1 > this.state.data.pages - 1) {
return this.setState({ page: this.state.data.pages });
}
this.setState({ page: this.state.data.page + 1 });
};
onPrevPage = () => {
if (this.state.page - 1 < 1) {
return this.setState({ page: 1 });
}
this.setState({ page: this.state.page - 1 });
};
render() {
return this.props.children?.({
state: this.state,
reload: this.list,
onChangeSearchText: this.onChangeSearchText,
onChangeParams: this.onChangeParams,
onNextPage: this.onNextPage,
onPrevPage: this.onPrevPage
});
}
}
export default ListHandler;
/*
Page and SearchText should be part of params?
*/
import React from "react";
import { Route, Switch } from "react-router-dom";
import get from "lodash/get";
import SectionLayout from "../SectionLayout";
import ListHandler, { ListHandlerChildrenProps, ListHandlerListFunction } from "../ListHandler";
import FormHandler, { FormHandlerChildrenProps } from "../FormHandler";
import DeleteModal from "../DeleteModal";
interface Props<T> {
label: string;
path: string;
push: (path: string) => void;
list: ListHandlerListFunction<T>;
load?: (id: string) => Promise<T>;
create?: (data: T) => Promise<T>;
update?: (data: T) => Promise<T>;
remove?: (id: string) => Promise<void>;
getDefaultState: () => T;
renderFormComponent(res: FormHandlerChildrenProps<T>): React.ReactNode;
renderListComponent(res: ListHandlerChildrenProps<T>): React.ReactNode;
}
class SectionHandler<T> extends React.Component<Props<T>> {
render() {
const {
label,
push,
path,
list,
renderListComponent,
getDefaultState,
load,
create,
update,
remove,
renderFormComponent
} = this.props;
return (
<ListHandler list={list}>
{(listProps) => (
<SectionLayout label={label}>
<Switch>
<Route path={path} exact>
{renderListComponent(listProps)}
</Route>
<Route path={[`${path}/new`, `${path}/:id`]}>
{({ match }) => (
<FormHandler
id={match?.params.id}
getDefaultState={getDefaultState}
load={load}
create={create}
update={update}
remove={remove}
onCreateSuccess={(data) => [push(`${path}/${get(data, "id", "")}`), listProps.reload()]}
onUpdateSuccess={() => listProps.reload()}
onRemoveSuccess={() => [push(path), listProps.reload()]}
>
{(formProps) => (
<>
<DeleteModal
open={formProps.state.modal === "delete"}
onClose={() => formProps.setModal("")}
onRemove={formProps.remove}
/>
{renderFormComponent({ ...formProps, remove: () => formProps.setModal("delete") })}
</>
)}
</FormHandler>
)}
</Route>
</Switch>
</SectionLayout>
)}
</ListHandler>
);
}
}
export default SectionHandler;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment