Numbers Probably Matter

    react-crud-admin

    1.0.48 • Public • Published

    react-crud-admin

    react-crud-admin is inspired by the Django Admin Interface. In the spirit of Django admin it opts for component customization by inheritance. The create-read-update-delete pattern is something that is encountered in a lot of apps. By creating a single component that lists entries and allows adding/changing it becomes possible to implement a DRY approach.

    Since React is primarily a UI library and there are a litany of backends, react-crud-admin does not implement any backend specific features but allows the developer to provide custom implementations.

    Getting Started

    Installation

    Install the library,

    npm install react-crud-admin
    

    Example

    If you are starting a react project you can use create-react-app to speed up bootstrapping,

    npm install create-react-app
    npx create-react-app example
    cd example
    

    then within the example directory,

    npm install react-crud-admin
    

    Create a new file example.js in the \src folder, in that file add the following lines

    import React from "react";
    import Admin from "react-crud-admin";
    import "react-crud-admin/css"; //optional css import
     
    export default class Example extends Admin {
      constructor() {
        super();
        this.name = "Contact"; // name of the objects
        this.name_plural = "Contacts"; // name of the objects in plural
        this.list_display_links = ["name"]; // which property of the object is clickable
        this.list_display = ["name", "number", "address.street"]; // a list of properties of the object to displayed on the list display page
      }
      get_queryset(page_number, list_per_page, queryset) {
        // the actual array containing objects to be displayed
        return [
          {
            id: 1,
            name: "Joe Next",
            number: "08939303003",
            address: { street: "Hallmark Street" }
          },
          {
            id: 2,
            name: "Isa Yoll",
            number: "0908839202",
            address: { street: "Barbican Street" }
          }
        ];
      }
    }

    then in index.js do

    import React from "react";
    import ReactDOM from "react-dom";
    import Example from "./example.js";
     
    ReactDOM.render(<Example />, document.getElementById("root"));

    The output in your browser should be

    example1

    This is the list display view. At this point adding and editing objects will not be possible since we have not configured the add/change view. To do that we need to import the react-jsonschema-form package.

    Edit example.js and add the following,

    import React from "react";
    import Admin from "react-crud-admin";
    import Form from "react-jsonschema-form";
    import "react-crud-admin/css"; //optional css import
     
    export default class Example extends Admin {
      constructor() {
        super();
        this.name = "Contact";
        this.name_plural = "Contacts";
        this.list_display_links = ["name"];
        this.list_display = ["name", "number", "address.street"];
      }
      get_queryset(page_number, list_per_page, queryset) {
        return [
          {
            id: 1,
            name: "Ken Next",
            number: "08939303003",
            address: { street: "Hallmark Street" }
          },
          {
            id: 2,
            name: "Isa Yoll",
            number: "0908839202",
            address: { street: "Barbican Street" }
          }
        ];
      }
      get_form(object = null) {
        let schema = {
          title: this.name,
          type: "object",
          required: ["name"],
          properties: {
            id: {
              type: "number",
              title: "id",
              default: Math.floor(1000 * Math.random()) + 1
            },
            name: { type: "string", title: "Name", default: "" },
            number: { type: "string", title: "Number", default: "" },
            address: {
              type: "object",
              title: "Address",
              properties: {
                street: { type: "string", title: "Street" }
              }
            }
          }
        };
     
        if (!object) {
          return <Form schema={schema} />;
        } else {
          return <Form schema={schema} formData={object} />;
        }
      }
    }

    the get_form method is passed the current object. The method returns a react-json-schema Form component. The schema object is used to define a schema for the Form (and to provide validation). See more on react-jsonschema-form at react-jsonschema-form. You can also read more on JSON Schema at JSON Schema Tutorial

    In get_form we check if an object exists displaying a preloaded form if it does and an empty one if it does not.

    The output in your browser after clicking the Add Contact button should be

    example21

    The output in your browser after clicking the first contact's name should be

    example2

    Guide

    List Display View

    Data

    Data is primarily injected through get_queryset.

    get_queryset(page_number, list_per_page, queryset);
    {
      return [
        {
          id: 1,
          name: "Ken Next",
          number: "08939303003",
          address: { street: "Hallmark Street" }
        },
        {
          id: 2,
          name: "Isa Yoll",
          number: "0908839202",
          address: { street: "Barbican Street" }
        }
      ];
    }

    The get_queryset method returns the queryset array including objects to be displayed in the current display page. Returning an array may not be useful when asynchronous network calls are made to a remote backend using AJAX or window.fetch. A class method of the Admin component set_queryset can be used for asynchronous calls.

    The example below returns the queryset in the current state object synchronously and sets a new queryset when the asynchronous call returns successfully. set_queryset invokes setState internally.

    get_queryset(page_number, list_per_page, queryset);
    {
      fetch("/path/to/backend", { method: "get" }).then(response => {
        if (response.ok) {
          this.set_queryset(response.results);
        }
      });
      return queryset;
    }

    It is important to note that get_queryset returns only items to be listed in the current display view/page. It is necessary to use the arguments page_number and list_per_page for fetching data from the backend. This allows us to implement pagination.

    get_queryset(page_number, list_per_page, queryset);
    {
      let backend = "/path/to/backend";
      let path =
        backend +
        "&limit=" +
        list_per_page +
        "&skip=" +
        (page_number - 1) * list_per_page;
      fetch(path, { method: "get" }).then(response => {
        if (response.ok) {
          response.json().then(results => {
            this.set_queryset(results.data);
            this.set_total(results.total);
          });
        }
      });
     
      return queryset;
    }

    set_total is used to set the total number of items available. For example if list_per_page is 10 and there are a total 100 records in the backend results.data will have 10 items and results.total is 10. Please note that it is the responsibility of the backend to return these values in whatever format.

    set_queryset is just,

    set_queryset(queryset);
    {
      this.setState({ queryset: queryset });
    }

    and set_total is ,

    set_total(total);
    {
      this.setState({ total: total });
    }

    It is also possible to set a fixed queryset using queryset property.

    this.queryset = [
      {
        id: 1,
        name: "Ken Next",
        number: "08939303003",
        address: { street: "Hallmark Street" }
      },
      {
        id: 2,
        name: "Isa Yoll",
        number: "0908839202",
        address: { street: "Barbican Street" }
      }
    ];

    This is always overriden by get_queryset.

    UI Customisation

    Display

    List Display

    The get_list_display method returns a list/array of properties/field names of the objects in the queryset to be displayed on the list display page. It can be overridden by the member variable list*display. A property is any string that should exist in the objects within a queryset and works with lodash's *.at function. See more at Lodash

    let object={ name : "any name",{ address : { street : "any"}},emails: ["any@any.com"]}

    The properties "name","address.street" and "emails[0]" are all acceptable. In our example we use,

    this.list_display = ["name", "number", "address.street"];

    We can also use,

    get_list_display();
    {
      return ["name", "number", "address.street"];
    }
    List Display Links

    The get_list_display_links method returns the list/array of properties of the objects in the queryset that are clickable (and redirect to the add/change view) when displayed on the list display page. It can be overridden by the member variable list_display_links. A property is any string that should exist as a property in the objects within a queryset and works with lodash's _.at function.

    In our example we use,

    this.list_display_links = ["name"];

    in the constructor but we could have used,

    get_list_display_links(){
     
          return ['name']
     
    }

    Header Transforms

    Table headers in the list display view are obtained from property names in get_list_display method by default. There are certain cases when one would like to customise these headers. get_header_transforms does just that. As an example

    get_header_transforms();
    {
      return {
        name: function(header) {
          return "Contact " + header;
        }
      };
    }

    Should produce

    example3

    Header transforms can be defined using the instance variable header_transforms (which is always overridden by get_header_transforms).

    this.header_transforms = {
      name: function(header) {
        return "Contact " + header;
      }
    };

    In summary get_header_transforms returns an object whose properties are field names corresponding to properties of any object in the queryset and whose values are transform functions.

    Field Transforms

    Field transforms return an object whose properties are field names corresponding to properties of any object in the queryset and whose values are transform functions. They are used to transform the values of objects within the queryset. For example to transform every "name" property of all the objects in the queryset to lower case we use,

    get_field_transforms();
    {
      return {
        name: function(content, object) {
          return content.toLowerCase();
        }
      };
    }

    or

    this.field_transforms = {
      name: function(content, object) {
        return content.toLowerCase();
      }
    };

    The first argument of the transform function is the content to be displayed while the second argument is the current object.

    This produces

    example31

    All the contact names have been turned to lower case. Field transforms apply to full columns on the data table in the display view. It is possible to replace fields with React components like images and buttons. As an example suppose a column has fields which contain image urls, we can replace each field with a corresponding image.

    this.field_transforms = {
      image_url: function(content, object) {
        return <img src={content} />;
      }
    };

    Extra Fields

    It is sometimes desirable to create new fields that are not properties of any of the objects in the queryset. As an example, we create a new field that displays the current time using the moment library.

    get_extra_fields();
    {
      return {
        now: function(object, label) {
          return moment(new Date()).format("LLL");
        }
      };
    }

    or somewhere in the constructor,

    this.extra_fields = {
      now: function(object, label) {
        return moment(new Date()).format("LLL");
      }
    };

    The get_extra_fields method returns an object whose properties are extra field names not corresponding to properties of any object in the queryset and whose values are display functions. This will create extra fields that are not tied to objects. Extra fields have to be manually included in the get_list_display in order to appear in the list display page.

    Adding the get_extra_fields method is not enough to display the newly created field. We must add the field to list_display .

    this.list_display = ["name", "number", "address.street", "now"];

    Don't forget to add import moment from "moment"; at the top of the file. The output is,

    example32

    Pagination

    get_list_per_page method returns the number of items to be listed in a page. Can be overridden by list_per_page.

    this.list_per_page = 10;

    or

    get_list_per_page();
    {
      return 10;
    }

    Search

    Search is not implemented by default. One has to implement the search(term,queryset) method. From our previous example, we can implement search by matching the search term with the "name" property of all objects in the queryset. The method returns a filtered queryset. Optionally, if search requires an asynchronous call to a backend, one can use set_queryset.

    search(term, queryset);
    {
      let filtered_queryset = [];
      for (var object of queryset) {
        if (object.name.search(new RegExp(term, "i")) >= 0) {
          filtered_queryset.push(object);
        }
      }
      return filtered_queryset;
    }

    or asynchronously

    search(term, queryset);
    {
      fetch("/path/to/backend/search?q=" + term, { method: "get" }).then(
        response => {
          if (response.ok) {
            this.set_queryset(response.results);
          }
        }
      );
      return queryset; //remember to return queryset synchronously
    }

    We can enable live search by

    this.live_search = true;

    default is false.

    Sorting

    Sorting is not implemented by default. Sorting can be achieved by implementing the sort_by method,

    sort_by(sort_fields, queryset);
    {
    }

    sort_fields is an array containing objects whose properties are properties of objects in the queryset and whose values are either "asc" or "desc". As an example, for a queryset

    [
      {
        id: 1,
        name: "Joe Next",
        number: "08939303003",
        address: { street: "Hallmark Street" }
      },
      {
        id: 2,
        name: "Isa Yoll",
        number: "0908839202",
        address: { street: "Barbican Street" }
      }
    ];

    we can have sort_fields equal to

    [{ name: "asc" }, { number: "desc" }, { address: "asc" }];

    For our example, we implement sort_by using,

    sort_by(sort_fields, queryset); //from adminjs
    {
      let item = sort_fields[sort_fields.length - 1];
     
      let pairs = _.toPairs(item);
      let field_names = pairs.map(item => item[0]);
      let field_orders = pairs.map(item => item[1]);
     
      return _.orderBy(queryset, field_names, field_orders);
    }

    We use the lodash library to sort the current queryset and return an updated one. We only use the latest sort order by examining the last item of the sort_fields array. In a practical application, a backend will perform the sort operations and we will use set_queryset.

        sort_by(sort_fields,queryset)
        {
        fetch('/path/to/backend/sort',{
             'method': 'post',
             'body': JSON.stringify(sort_fields)}).then((response)
             {
                 if(response.ok}
                  {
                response.json().then((results)=>
                {
     
                this.set_queryset(results.data);
     
                })
     
                       }
             })
     
          return  queryset;
        }

    It is also important to define the member variable is_object_equal which defines equality between objects in a queryset. This variable is used to test if an object has been previously selected using the radio buttons in the display view. By default,is_object_equal is

    this.is_object_equal = function(a, b) {
      return a == b;
    };

    This may not be sufficient for some applications. For example we might want to use the object's id property to check equality.

    Sorting can achieved in the list display view by clicking on the table headers corresponding to property names.

    this.is_object_equal = function(a, b) {
      return a.id == b.id;
    };

    Actions

    get_actions returns an actions object whose properties are action names and values are action methods. get_actions can be overridden by the actions member variable.

    Each actions object property (e.g. "delete") is passed an array of selected objects. One can then handle those objects. Actions will appear on the list display page within a dropdown. The default "delete" action method is not implemented.

    We can implement the delete action for our previous example,

    import React from 'react';
    import Admin from "react-crud-admin";
    import Form from "react-jsonschema-form";
    import moment from "moment";
    import "react-crud-admin/css";//optional css import
     
    var data=[
            {id: 1, name: 'Ken Next', number: '08939303003',address:{ street: "Hallmark Street"}},
                {id: 2,name: 'Isa Yoll', number: '0908839202',address:{ street: "Barbican Street"}}
    ];
    export default class Example extends Admin
    {
        ......
        ......
        ......
        get_queryset()
        {
            return data
            }
     
        get_actions()
            {
              return { "delete" : (selected_objects)=>{
                                   let total=data.length;
                                       for(let object of selected_objects)
            {
                  data.splice(data.indexOf(object),1);
     
                                       }
                       this.set_queryset(data);
                       this.set_total(total-selected_objects.length);
                  }
              }
     
        }
     
    }

    Filters

    get_filters can be used to implement filters for the queryset.

    get_filters();
    {
      return {
        by_street_name: {
          options: [
            { value: "Hallmark Street", label: "Hallmark Street" },
            { value: "Barbican Street", label: "Barbican Street" }
          ],
          filter_function: (option, queryset) => {
            let grouped = _.groupBy(queryset, "address.street");
     
            return _.has(grouped, option.value) ? grouped[option.value] : [];
          }
        },
        by_id: {
          options: [
            { value: "even", label: "even" },
            { value: "odd", label: "odd" }
          ],
          filter_function: (option, queryset) => {
            let grouped = _.groupBy(queryset, contact => {
              return contact.id % 2 == 0 ? "even" : "odd";
            });
     
            return _.has(grouped, option.value) ? grouped[option.value] : [];
          }
        }
      };
    }

    The get_filters method returns an object whose properties are filter names. The example above returns 2 filters. The first is a filter "by_street_names" that filters based on the street addresses of the contacts. Each filter object has two properties : "options" and "filter_function". "options" is an array of options objects which have a "value" and "label" properties. The "label" will appear in the UI while the "value" is used for processing. This is similar to the select HTML tag. For asynchronous operations, a method to set the options of a filter is provided. set_filter_options takes two arguments,

    set_filter_options(filter_name, options);

    the name of the filter and the options array of the filter.

    The "filter_function" is a function that performs filtering. It has two arguments, "option" which is the selected "option" and the current queryset. It must return a queryset. "option" is one of "options" array. For asynchronous cases, set_queryset and set_total methods can be used to set the queryset after backend filtering.

    The second filter "by_id" filters the queryset by even and odd id property. The filter UI uses React Select component.

    Add/Change View

    Starting from version 1.0.48 the add/change view is presented in a modal.

    Forms

    We can add submit handlers for the form in our example,

    ....
    ....
    ....
    export default class Example extends Admin
    {
     
    .....
    .....
    .....
     
     get_form(object=null)
        {
         let schema = {
            title: this.name,
            type: "object",
            required: ["name"],
            properties: {
            id: {type: "number", title: "id", default: Math.floor(1000*Math.random())+1 },
            name: {type: "string", title: "Name", default: ""},
            number : {type: "string", title: "Number", default: ""},
            address : {type: "object", title: "Address",
                   properties : {
                          street : { type : "string",title : "Street"}
                    }
                }
              }
            };
     
         if(!object)
         {
               return <Form schema={schema} onSubmit={this.form_submit.bind(this)} />
         }
         else
         {
                return <Form schema={schema}  formData={object} onSubmit={this.form_submit.bind(this)} />
         }
         }
    ....
    ....
    ....
        form_submit(form)
        {
        let contact=form.formData;
     
        if(form.edit)
        {
     
            this.state.queryset.splice(this.state.queryset.indexOf(this.state.object),1,contact);
            this.response_change();
        }
        else
        {
            this.state.queryset.push(contact);
            this.response_add();
        }
        }
     
    ......
    ......
    ......
    }

    The submit handler checks to see if an object is being edited and replaces it with the edited object. If the object is being added, the method appends it to the state.queryset object.

    Post Submit

    response_add or response_change can be called after a new object is created or edited respectively. These functions should implement behaviour after a successful form submission. By default these functions are both defined as,

    function()
    {
      this.setState({display_type :display_type.list,
                     object :null,
                     queryset: this.get_queryset(this.state.page_number,this.list_per_page,this.state.queryset)
               });
    }

    This sets the display type to "list", changes the object to null and invokes get_queryset to fetch the latest data.

    Permissions

    has_add_permission , has_change_permission and has_module_permission methods are used to control access.

    The has_add_permission method takes no arguments and should return a boolean that signifies wether the user can add a new object. It simply hides the Add button.

    The has_change_permission method takes the current object being edited, state.object, as an argument and should return a boolean that signifies wether the user can edit an object. It simply disables all list_display_links.

    The has_module_permission method takes no arguments and should return a boolean that signifies wether the user should load the component. It simply shows an error message after an unauthorized access attempt.

    Miscellany

    state object

    The default state object has the following properties,

    Property Default Value Description
    queryset [] Can be set with set_queryset
    page_number 1
    total 0 Total number of items. Can be set with set_total.
    display_type "list" Current display. Can be "list" or "change" for add/change view
    object null Current object to be edited (or created) in the add/change view
    selected_objects Empty set The Set class a wrapper around array that implements set logic

    Care must be taken note to override the state object values especially at initialization stages e.g. in the constructor or component_will_mount .

    Progress Indicator

    show_progress and hide_progress can be used in the both the list display and add/change view to display and hide a progress indicator. render_progress can be used to override the default progress indicator component to be rendered. render_progress has one argument which is a boolean denoting whether or not to render a progress indicator. For example,

    render_progress(loading);
    {
      if (loading) {
        return <progress />;
      }
    }

    render_progress should return a component.

    ESDOC API Documentation.

    The API documentation generated with esdoc.js is available here.

    Component Props

    There are many instances where customising the react-crud-admin component using component props is beneficial. Perhaps the component is to be used as a child component that reacts to changes in the parent component. For those cases it is possible to use props to customise the Admin component.

    All members and properties of the Admin component class can be assigned using React component props. All functions passed as props are automatically bound to the Admin thus making them class members. This means that this will reference the Admin component despite being used outside the component.

    The end result will look like,

    <Example
      queryset={data}
      search={search}
      form_submit={form_submit}
      get_form={get_form}
      get_header_transforms={get_header_transforms}
      get_field_transforms={get_field_transforms}
      get_extra_fields={get_extra_fields}
      get_actions={get_actions}
      get_filters={get_filters}
      name={name}
      name_plural={name_plural}
      list_display={list_display}
      list_display_links={list_display_links}
    />

    You can use bind_exclude prop which is an array that contains names of methods that should be excluded from automatic binding, allowing this reference to refer to something else.

    A complete example is available at link.

    Example

    The file containing the example used here is available at link.

    Advanced

    Rendering

    There are two main render like methods that combine to form the Component.render method of react-crud-admin. The first is render_list_page which renders the current list view and the second is render_change_page which renders the add/change view. Only one of these views is active at a time and forms the Component.render for the component. A full implementation is given below,

    render();
    {
      if (!this.has_module_permission()) {
        return (
          <div>
            {" "}
            <h1> Permission Denied </h1>
          </div>
        );
      }
     
      if (this.state.display_type == display_type.list) {
        return this.render_list_page();
      } else {
        //Important: the next two lines are for URL navigation and handling the browser back button
        this._change_uuid = uuidv1();
        history.pushState(
          {},
          "Change View",
          window.location.hash + "/change/" + this._change_uuid
        );
     
        return this.render_change_page();
      }
    }

    You can also listen to events that are triggered when the display is about to change or has changed,

    display_will_change(display_type,object)
    {
     
    }

    and

    display_changed(display_type,object)
    {
     
    }

    display_type is either "list" or "change".

    List Page

    It is possible to add components above and below the list view using render_above_list_view and render_below_list_view methods. Both methods take no arguments and by default return null. These methods can be overridden to return components.

    render_list_page();
    {
      return (
        <div>
          {this.render_above_list_view()}
          {this.render_list_view()}
          {this.render_below_list_view()}
        </div>
      );
    }

    Within the render_list_view method itself, customisations are possible. The full implementation of render_list_view is,

    render_list_view();
    {
      return (
        <div>
          {this.render_add_button()}
          {this.render_below_add_button()}
          {this.render_search_field()}
          {this.render_below_search_field()}
          {this.render_actions()}
          {this.render_below_actions()}
          {this.render_filters()}
          {this.render_below_filters()}
          {this.render_table()}
          {this.render_below_table()}
          {this.render_progress(this.state.loading)}
          {this.render_below_progress()}
          {this.render_pagination()}
        </div>
      );
    }

    The render_below_* methods are used to add components below the add button, search field, actions,filters,list table and progress indicator. By default these methods return null. These methods can be overridden to return components.

    It is also worth noting that one can hide any of the components by the overriding their methods and returning null. For example to hide the actions component, we override the render_actions method.

    render_actions();
    {
    }

    The Add/Change Page

    It is possible to add components above and below the add/change view using render_above_change_view and render_below_change_view methods. Both methods take no arguments and by default return null. These methods can be overridden to return components. The full implementation is

    render_change_page();
    {
      return (
        <div>
          {this.render_above_change_view()}
          {this.render_change_view()}
          {this.render_below_change_view()}
        </div>
      );
    }

    Keywords

    none

    Install

    npm i react-crud-admin

    DownloadsWeekly Downloads

    19

    Version

    1.0.48

    License

    ISC

    Unpacked Size

    15 MB

    Total Files

    131

    Last publish

    Collaborators

    • mjibril