react-simple-crud-ui

Written in React with hooks, simple CRUD UI with authentication.

Usage no npm install needed!

<script type="module">
  import reactSimpleCrudUi from 'https://cdn.skypack.dev/react-simple-crud-ui';
</script>

README

One example worths thoudsands line of explanations.

App.tsx

import React, { useReducer, useState } from 'react';
import Axios from 'axios';
import './App.css';

import CountryForm from './CountryForm';
import LanguageForm from './LanguageForm';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer, toast } from 'react-toastify';

import RSCU from 'react-simple-crud-ui';
import { CrudSettingModel, AuthSettingModel } from 'react-simple-crud-ui/dist/CRUD/Model';

function App() {
  const AuthenticationLayer = RSCU.components.AuthenticationLayer;
  const CrudStateModel = RSCU.models.CrudStateModel;
  
  const CRUD = RSCU.components.CRUD;
  const [appState, dispatchApp] = useReducer(RSCU.reducers.appReducer, new RSCU.models.AppStateModel());

  const reducer = {
    appState,
    dispatchApp
  }

  const countrySetting = {
    initialCrudState: new CrudStateModel(10, 'Country'),
    Form: CountryForm,
    ListGenerator: (crudState, dispatchCrud, mode) => {
      return (
        <div>
          <div className="row bold border-bottom">
            <div className="col-4">
              Country Name
              </div>
            <div className="col-4">
              Code
              </div>
            <div className="col-4">
              Year
              </div>
          </div>
          <div className="row bold border-bottom">
            <div className="col-4">
              <input className="form-control" placeholder="Filter By Name" defaultValue={crudState.filter.value} onChange={(e) => dispatchCrud({ type: 'FILTER', payload: { value: e.target.value } })} />
            </div>
            <div className="col-4">
            </div>
            <div className="col-4">
            </div>
          </div>
          {
            crudState.allDataPagination.map(i => {
              const key = new Date().getTime() + '' + i.id;
              return (
                <div key={key} className="row data-record" title="Click to edit"
                  onClick={() => dispatchCrud({ type: 'CHANGE_MODE', payload: mode.Edit, editId: i.id })}>
                  <div className="col-4 grid-left ellipsis">
                    {i.name}
                  </div>
                  <div className="col-4 grid-center ellipsis">
                    {i.code}
                  </div>
                  <div className="col-4 grid-right ellipsis">
                    {i.year}
                  </div>
                </div>
              )
            })
          }
        </div>
      )
    },
    filterFn: (item: any, filterObject: any) => {
      return item.name.toLowerCase().includes(filterObject.value);
    },
    loadAllData: (appReducer, crudReducer, mounted: boolean) => {
      const { appState, dispatchApp } = appReducer;
      const { crudState, dispatchCrud } = crudReducer;
      Axios.post("https://some-graphql-api-endpoint", {
        query: `query {
                    countries {
                        id
                        code
                        name
                        year
                        flagUrl
                        createdDate
                        updatedDate
                        userId
                    }
                }`
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(axiosData => {        
        const data = axiosData.data;
        if (data.data !== undefined && data.data !== null) {
          if (!!mounted) {
            dispatchCrud({ type: 'GET_ALL_SUCCESS', payload: data.data.countries })
            if (crudState.filter !== '') {
              dispatchCrud({ type: 'FILTER', payload: crudState.filter })
            }            
          } else {
            console.log('list is unmounted ', mounted);
          }
        } else {
          if (!!mounted) {
            dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(data) })
          } else {
            console.log('list is unmounted ', mounted);
            console.log(data);
          }          
        }
      }).catch(
        err => {
          console.log('list is unmounted ', mounted);
          console.log(err);
          if (!!mounted) {
            dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(err) });
          }
          localStorage.removeItem('account');
          dispatchApp({ type: 'LOGOUT' });
        }
      );
    },
    addCrud: (appReducer, crudReducer, data) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;

      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `mutation createCountry($data: CountryModel!) {
                    createCountry(data: $data) {
                        id
                        code
                        name
                        year
                        flagUrl
                        createdDate
                        updatedDate
                        userId
                    }
                }
                
                `,
        variables: {
          "data": { ...data, year: parseInt(data.year) }
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== undefined && data.data !== null) {
          // const createCountry = data.data.createCountry;
          // console.log(createCountry);
          toast.success("Country is created!");
        } else {
          console.log(data);
          toast.warning("Create failed!");
        }
        dispatchCrud({ type: 'END' });
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });
        toast.error("Country can not be created! Something goes wrong here!");
      })
    },
    loadOneCrud: (appReducer, crudReducer, setThisCrud) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;
      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `query country($id: ID!) {
                    country(id: $id) {
                        id
                        code
                        name
                        year
                        flagUrl
                        createdDate
                        updatedDate
                        userId
                    }
                }`,
        variables: {
          "id": crudState.editId
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== null && data.data !== undefined) {
          const country = data.data.country;
          // console.log(country);
          setThisCrud(country);
          dispatchCrud({ type: 'END' });
        } else {
          console.log('error ', data);
          dispatchCrud({ type: 'END' });
        }
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });

      })
    },
    editCrud: (appReducer, crudReducer, data) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;
      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `mutation updateCountry($data: CountryModel!) {
                    updateCountry(data: $data) {
                        id
                        code
                        name
                        year
                        flagUrl
                        createdDate
                        updatedDate
                        userId
                    }
                }
                `,
        variables: {
          "data": { ...data, year: parseInt(data.year) }
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== undefined && data.data !== null) {
          // const updateCountry = data.data.updateCountry;
          // console.log(updateCountry);
          toast.success("Country is updated!");
        } else {
          console.log(data);
          toast.warning("Update failed");
        }
        dispatchCrud({ type: 'END' });
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });
        toast.error("Country can not be updated! Something goes wrong here!");

      })
    },
    deleteCrud: (appReducer, crudReducer, data) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;
      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `mutation deleteCountry($data: CountryModel!) {
                    deleteCountry(data: $data) {
                        id
                        code
                        name
                        year
                        flagUrl
                    }
                }
                
                `,
        variables: {
          "data": {
            "id": data.id
          }
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== undefined && data.data !== null) {
          // const deleteCountry = data.data.deleteCountry;
          // console.log(deleteCountry);
          toast.success("Country is deleted!");
        } else {
          console.log(data);
          toast.warning("Delete failed");
        }
        dispatchCrud({ type: 'END' });
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });
        toast.error("Country can not be deleted! Something goes wrong here!");

      })
    }

  } as CrudSettingModel;

  const languageSetting: CrudSettingModel = {
    initialCrudState: new CrudStateModel(10, 'Language'),
    Form: LanguageForm,
    ListGenerator: (crudState, dispatchCrud, MODE) => {
      return (
        <div>
          <div className="row bold border-bottom">
            <div className="col-4">
              Name
              </div>
            <div className="col-4">
              Native Name
              </div>
            <div className="col-4">
              Code
              </div>
          </div>
          <div className="row bold border-bottom">
            <div className="col-4">
              <input className="form-control" placeholder="Filter By Name" defaultValue={crudState.filter.value} onChange={(e) => dispatchCrud({ type: 'FILTER', payload: { value: e.target.value } })} />
            </div>
            <div className="col-4">
            </div>
            <div className="col-4">
            </div>
          </div>
          {
            crudState.allDataPagination.map(i => {
              const key = new Date().getTime() + '' + i.id;
              return (
                <div key={key} className="row data-record" title="Click to edit"
                  onClick={() => dispatchCrud({ type: 'CHANGE_MODE', payload: MODE.Edit, editId: i.id })}>
                  <div className="col-4 grid-left ellipsis">
                    {i.name}
                  </div>
                  <div className="col-4 grid-center ellipsis">
                    {i.nativeName}
                  </div>
                  <div className="col-4 grid-right ellipsis">
                    {i.code}
                  </div>
                </div>
              )
            })
          }
        </div>
      )
    },
    filterFn: (item, filterObject) => {
      return item.name.toLowerCase().includes(filterObject.value);
    },
    loadAllData: (appReducer, crudReducer, mounted) => {
      const { appState, dispatchApp } = appReducer;
      const { crudState, dispatchCrud } = crudReducer;
      Axios.post("https://some-graphql-api-endpoint", {
        query: `query {
                languages {
                    id
                    code
                    name
                    nativeName
                    createdDate
                    updatedDate
                    userId
                }
            }`
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
            const data = i.data;
            // console.log(data);
            if (data.data !== undefined && data.data !== null) {
              if (!!mounted) {
                dispatchCrud({ type: 'GET_ALL_SUCCESS', payload: data.data.languages })
                if (crudState.filter !== '') {
                  dispatchCrud({ type: 'FILTER', payload: crudState.filter })
                }
              } else {
                console.log('unmounted');
              }
            } else {
              console.log(data);
              if (!!mounted) {
                dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(data) })
              } else {
                console.log('unmounted');
              }
            }
          }
        ).catch(
          i => {
            console.log(i);
            if (!!mounted) {
              dispatchCrud({ type: 'GET_ALL_FAIL', payload: JSON.stringify(i) })
            } else {
              console.log('unmounted');
            }
            localStorage.removeItem('account');
            dispatchApp({ type: 'LOGOUT' });
          }
        );
      
    },
    addCrud: (appReducer, crudReducer, data) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;
      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `mutation createLanguage($data: LanguageModel!) {
                    createLanguage(data: $data) {
                        id
                        code
                        name
                        nativeName
                        createdDate
                        updatedDate
                        userId
                    }
                }`,
        variables: {
          "data": data
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== undefined && data.data !== null) {
          // const createLanguage = data.data.createLanguage;
          // console.log(createLanguage);
          toast.success("Language is created!");
        } else {
          console.log(data);
          toast.warning("Create failed!");
        }
        dispatchCrud({ type: 'END' });
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });
        toast.error("Language can not be created! Something goes wrong here!");
      })
    },
    loadOneCrud: (appReducer, crudReducer, setThisCrud) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;
      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `query language($id: ID!) {
                    language(id: $id) {
                        id
                        code
                        name
                        nativeName
                        createdDate
                        updatedDate
                        userId
                    }
                }`,
        variables: {
          "id": crudState.editId
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== null && data.data !== undefined) {
          const language = data.data.language;
          // console.log(language);
          setThisCrud(language);
          dispatchCrud({ type: 'END' });
        } else {
          console.log('error ', data);
          dispatchCrud({ type: 'END' });
        }
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });

      })
    },
    editCrud: (appReducer, crudReducer, data) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;
      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `mutation updateLanguage($data: LanguageModel!) {
                    updateLanguage(data: $data) {
                        id
                        code
                        name
                        nativeName
                        createdDate
                        updatedDate
                        userId
                    }
                }`,
        variables: {
          "data": data
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== undefined && data.data !== null) {
          // const updateLanguage = data.data.updateLanguage;
          // console.log(updateLanguage);
          toast.success("Language is updated!");
        } else {
          console.log(data);
          toast.warning("Update failed");
        }
        dispatchCrud({ type: 'END' });
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });
        toast.error("Language can not be updated! Something goes wrong here!");

      })
    },
    deleteCrud: (appReducer, crudReducer, data) => {
      // eslint-disable-next-line
      const { appState, dispatchApp } = appReducer;
      // eslint-disable-next-line
      const { crudState, dispatchCrud } = crudReducer;
      dispatchCrud({ type: 'START' });
      Axios.post("https://some-graphql-api-endpoint", {
        query: `mutation deleteLanguage($data: LanguageModel!) {
                    deleteLanguage(data: $data) {
                        id
                        code
                        name
                        nativeName
                    }
                }
                
                `,
        variables: {
          "data": {
            "id": data.id
          }
        }
      }, {
        headers: {
          "token": appState.account.token
        }
      }).then(i => {
        const data = i.data;
        // console.log(data);
        if (data.data !== undefined && data.data !== null) {
          // const deleteLanguage = data.data.deleteLanguage;
          // console.log(deleteLanguage);
          toast.success("Language is deleted!");
        } else {
          console.log(data);
          toast.warning("Delete failed");
        }
        dispatchCrud({ type: 'END' });
      }).catch(i => {
        console.log(i);
        dispatchCrud({ type: 'END' });
        toast.error("Language can not be deleted! Something goes wrong here!");

      })
    }
  } as CrudSettingModel;

  const MODE = {
    COUNTRY: "COUNTRY",
    LANGUAGE: "LANGUAGE"
  }
  const [currentMode, setCurrentMode] = useState(MODE.COUNTRY);
  const isCountryMode = currentMode === MODE.COUNTRY;
  const isLanguageMode = currentMode === MODE.LANGUAGE;

  const authSetting = {
    app: {
      title: 'Demo Site',
      loginTitle: 'Gateway'
    },
    components: {
      CustomComponentGenerator: () => {
        if (!!appState.auth) {
          return (
            <div className="width-100" style={{ marginTop: '20px' }}>

              <button type="button" className={'btn ' + (!!isCountryMode ? 'btn-success' : 'btn-dark')} onClick={() => setCurrentMode(MODE.COUNTRY)}>Country</button>
              <button type="button" className={'btn ' + (!!isLanguageMode ? 'btn-success' : 'btn-dark')} onClick={() => setCurrentMode(MODE.LANGUAGE)}>Language</button>
              <hr></hr>
              {!!isCountryMode ? <CRUD setting={countrySetting} reducer={reducer} /> : ''}
              {!!isLanguageMode ? <CRUD setting={languageSetting} reducer={reducer} /> : ''}
            </div>
          )
        }
        return (
          <div>

          </div>
        );
      }
    },
    login: {
      url: "https://some-authenticated-graphql-api-endpoins",
      data: (username: string, password: string) => {
        return {
          query: `
          query login($username: String!, $password: String!) {
          login(username: $username, password: $password) {
              _id
              token
              username
              firstName
              lastName
              email
              role
          }
          }
          `,
          variables: {
            username: username,
            password: password
          }
        }
      },
      headers: {}
    },


  } as AuthSettingModel;

  return (
    <div className="App container">
      <AuthenticationLayer reducer={reducer} setting={authSetting} />
      <ToastContainer />
    </div>
  )
}

export default App;

CountryForm.tsx

import React from 'react';
import { useForm } from 'react-hook-form';
function CountryForm(props: any) {
    const { id: inputId, name: inputName, code: inputCode, flagUrl: inputFlagUrl, year: inputYear, updatedDate: inputUpdatedDate } = props.form;
    const { onSubmit, onSubmitDeleting } = props;
    const { register, handleSubmit, errors } = useForm();
    const currentUpdatedDate = () => {
        if (inputUpdatedDate === undefined) {
            return new Date().toDateString() + ' ' + new Date().toTimeString();
        }
        return new Date(parseInt(inputUpdatedDate)).toDateString() + ' ' + new Date(parseInt(inputUpdatedDate)).toTimeString()
    }
    const onFormSubmit = (data: any) => {        
        // console.log('on form submit, using handle submit from useForm, ', data);
        onSubmit(inputId === undefined || inputId === '' ? data : { id: inputId, ...data});
    }
    const onFormSubmitDeleting = (e: any) => {
        e.preventDefault();
        onSubmitDeleting({ id: inputId });
    }
    return (
        <form onSubmit={handleSubmit(onFormSubmit)}>
        <div className="form-row">
            <div className="form-group col-md-6">
            <label htmlFor="name">Name</label>
            <input type="text" className="form-control" id="name" name="name" defaultValue={inputName} ref={register({ required: true })} />
            <p className="error">{errors.name && "Name is required"}</p>
            </div>
            <div className="form-group col-md-6">
            <label htmlFor="code">Code</label>
            <input type="text" className="form-control" id="code" name="code" defaultValue={inputCode} ref={register({ required: true })}/>
            <p className="error">{errors.code && "Code is required"}</p>
            </div>
        </div>
        <div className="form-group">
            <label htmlFor="flagUrl">Flag URL</label>
            <input type="text" className="form-control" id="flagUrl" name="flagUrl" placeholder="https://photo.jpg" defaultValue={inputFlagUrl} ref={register({ required: true })} />
            <p className="error">{errors.flagUrl && "Flag URL is required"}</p>
        </div>
        <div className="form-row">
            <div className="form-group col-md-6">
            <label htmlFor="year">Year</label>
            <input type="number" className="form-control" id="year" name="year" defaultValue={inputYear} ref={register({ required: true, min: 1200, max: 2222 })} />
            <p className="error">{errors.year?.type === "required" && "Year is required"}</p>
            <p className="error">{errors.year?.type === "min" && "Year must be greater than 1200"}</p>
            <p className="error">{errors.year?.type === "max" && "Year must be less than 2222"}</p>
            </div>
            <div className="form-group col-md-6">
            <label htmlFor="updatedDate">Latest Update Date</label>
            <input type="text" disabled className="form-control" id="updatedDate" readOnly defaultValue={currentUpdatedDate()} />
            </div>
        </div>
        
        <button type="submit" className="btn btn-primary">Submit</button>
        <hr></hr>
        { inputId !== undefined ? <button className="btn btn-danger" onClick={onFormSubmitDeleting}>Delete</button> : '' }
        </form>
    )
}

export default CountryForm;

LanguageForm.tsx

import React from 'react';
import { useForm } from 'react-hook-form';
function LanguageForm(props: any) {
    // console.log(props);
    const { id: inputId, name: inputName, code: inputCode, nativeName: inputNativeName, updatedDate: inputUpdatedDate } = props.form;
    const { onSubmit, onSubmitDeleting } = props;
    const { register, handleSubmit, errors } = useForm();
    const currentUpdatedDate = () => {
        if (inputUpdatedDate === undefined) {
            return new Date().toDateString() + ' ' + new Date().toTimeString();
        }
        return new Date(parseInt(inputUpdatedDate)).toDateString() + ' ' + new Date(parseInt(inputUpdatedDate)).toTimeString()
    }
    const onFormSubmit = (data: any) => {        
        // console.log('on form submit, using handle submit from useForm, ', data);
        onSubmit(inputId === undefined || inputId === '' ? data : { id: inputId, ...data});
    }
    const onFormSubmitDeleting = (e: any) => {
        e.preventDefault();
        onSubmitDeleting({ id: inputId });
    }
    return (
        <form onSubmit={handleSubmit(onFormSubmit)}>
        <div className="form-row">
            <div className="form-group col-md-6">
            <label htmlFor="name">Name</label>
            <input type="text" className="form-control" id="name" name="name" defaultValue={inputName} ref={register({ required: true })} />
            <p className="error">{errors.name && "Name is required"}</p>
            </div>
            <div className="form-group col-md-6">
            <label htmlFor="code">Code</label>
            <input type="text" className="form-control" id="code" name="code" defaultValue={inputCode} ref={register({ required: true })}/>
            <p className="error">{errors.code && "Code is required"}</p>
            </div>
        </div>
        <div className="form-group">
            <label htmlFor="nativeName">Native Name</label>
            <input type="text" className="form-control" id="nativeName" name="nativeName" placeholder="日本語" defaultValue={inputNativeName} ref={register({ required: true })} />
            <p className="error">{errors.nativeName && "Flag URL is required"}</p>
        </div>
        <div className="form-row">            
            <div className="form-group col-md-12">
            <label htmlFor="updatedDate">Latest Update Date</label>
            <input type="text" disabled className="form-control" id="updatedDate" readOnly defaultValue={currentUpdatedDate()} />
            </div>
        </div>
        
        <button type="submit" className="btn btn-primary">Submit</button>
        <hr></hr>
        { inputId !== undefined ? <button className="btn btn-danger" onClick={onFormSubmitDeleting}>Delete</button> : '' }
        </form>
    )
}

export default LanguageForm;