Close More (Vert) Close

ZingGrid is a feature-rich JavaScript library for visualizing and interacting with tabular data. The ZingGrid library is written in Vanilla JavaScript and utilizes web components. For some, this would suggest that it cannot be used with popular frameworks, but this article shows how you can get started using React and ZingGrid together and gives a good sense of how you might integrate it with your React application.

Note: The GitHub repository corresponding with this article can be found here.*

As previously mentioned ZingGrid is written in a neutral fashion in vanilla JavaScript and as such it can be used in any web application, with or without a framework. In the case of React we will not even need a wrapper to incorporate the library. To demonstrate that let's start out with create-react-app and quickly get a static grid displayed. We'll then go over some basics of using ZingGrid and React and finally we will conclude by walking thru the various pages in the ZingGrid-React demo demo which highlights how to integrate ZingGrid features like themes, events, and data binding within a React context. Do note this later code walk-thru is not an exhaustive one so you might want to have the demo set-up and explore it as you read.

Hello ZingGrid-React World

First, though, let's create a Hello World ZingGrid/React app. We'll start by creating a new React app, installing the ZingGrid component, and starting the app.

npx create-react-app zinggrid-react-helloworld
cd zinggrid-react-helloworld
npm install zinggrid
npm start

You can now view the app in a browser at localhost:3000 (check the output of npm start, it might be running on a different port).

The full demo contains 11 pages demonstrating different aspects of using ZingGrid in React. For this Hello World example, we'll just add a static grid to the App.js page.

Let's start by adding the import for ZingGrid:

zinggrid-react-helloworld/src/App.js
import 'zinggrid'

Now remove the contents of the App() function. You can also remove the import logo statement at this time. Replace the contents of App() with a return of a zing-grid component.

zinggrid-react-helloworld/src/App.js
function App() {
  return <zing-grid />
}

In the browser you should now see an empty grid, showing the text There are no records to display. and the Powered by ZingGrid logo. Let's define some tabular data. Above the return statement, add an array of objects called data. Note that each object in the array contains the same name and origin properties, these will become the columns of the grid:

zinggrid-react-helloworld/src/App.js
function App() {
  const data = [
    {name: "Philip J. Fry", origin: "Earth"},
    {name: "Turanga Leela", origin: "Earth"},
    {name: "Bender Bending Rodriguez", origin: "Earth"},
    {name: "Amy Wong", origin: "Mars"},
    {name: "Doctor John Zoidberg", origin: "Decapod 10"},
    {name: "Lord Nibbler", origin: "Earth"}
  ]

  return <zing-grid caption="Hello Futurama" data={JSON.stringify(data)} />
}

Note that we've added two attributes to the zing-grid component: caption and data. ZingGrid is expecting a JSON string as the value for data, so we do the conversion to JSON in the component definition (alternatively, we could have chosen to JSON.stringify the data array in a statement above the zing-grid component definition).

You should now see a grid in the browser! This app can be used to play around with all of the different grid options described in the ZingGrid docs. Let's move on from here to look at various aspects of using ZingGrid features in a React app.

zinggrid-react-demo

For this part of the article you should have the demo running locally. I'll hightlight key sections of the source code with a focus on the interaction between React and ZingGrid.

In an environment with nodejs, npm, and git installed:

git clone https://github.com/zinggrid-demos/zinggrid-react-demo.git
cd zinggrid-react-demo
npm install
npm run start

You should now be able to view the demo in your browser at localhost:3000. Each of the demo pages is contained in a file under zinggrid-react-demo/src/components.

Hello World (zinggrid-react-demo/src/components/Simple.js)

The Hello World demo is nearly identical to the simple example we created above in zinggrid-react-helloworld/src/App.js. There are a few minor differences:

  1. The component name has been changed from App to Simple.
  2. We use React's useState hook to define the data array, even though we're not modifying it yet.
  3. We've added the head-class attribute to the zing-grid component. This attaches the CSS class grid-header (defined in zinggrid-react-demo/src/App.css) to the header, making it appear in a bold font.

Themes (zinggrid-react-demo/src/components/Themes.js)

This demo shows how to use a React reference to a ZingGrid in order to set the data and modify the grid's visual theme after it has been created. We start by using React's useRef hook to declare a reference, and React's useState hook to declare the name of the theme. We then declare the table data, in this case as an array of arrays, one per row:

zinggrid-react-demo/src/components/Themes.js
const grid = useRef(null)
const [theme, setTheme] = useState('default')
const data = [
  [1,2,3],
  [4,5,6]
]

Here's the portion of the JSX for this component that defines the grid (the rest of the JSX creates the menu for selecting the theme):

zinggrid-react-demo/src/components/Themes.js
<zing-grid ref={grid} caption="Hello World" theme={theme} head-class="grid-header">
  <zg-colgroup>
    <zg-column index="0" header="Column 1"></zg-column>
    <zg-column index="1" header="Column 2"></zg-column>
    <zg-column index="2" header="Column 3"></zg-column>
  </zg-colgroup>
</zing-grid>

The reference to this grid is bound using the ref attribute to zing-grid and the theme is set using the theme attribute. The contents of this zing-grid consist of a zg-colgroup comprised of zg-column components that set the header for each column and its index into each of the rows of the data array.

When we obtain a reference to a ZingGrid component, all of the methods provided by the ZingGrid API are available, attached to the current property of the reference. In this case we use React's useEffect hook to invoke grid.current.setData(data) to set the table data for the grid. Note that when using setData we don't have to encode the data as JSON, as we do when using the data attribute.

The select menu:

zinggrid-react-demo/src/components/Themes.js
<select value={theme} onChange={ev => setTheme(ev.target.value)}>
  <option value="default">Default</option>
  <option value="android">Android</option>
  <option value="ios">iOS</option>
  <option value="dark">Dark</option>
  <option value="black">Black</option>
</select>

lists the theme options available in ZingGrid. The onChange handler sets theme to the selected theme name, which causes React to rerender the grid with the chosen theme.

Methods (zinggrid-react-demo/src/components/Methods.js)

The Methods demo further explores the use of ZingGrid methods, in this case allowing the editing of the table data via an HTML textarea.

We start by declaring the initial table data and two React references for the grid and textarea. Note that this data array consists of objects, each of which has breed and name properties.

zinggrid-react-demo/src/components/Methods.js
const dataSet = [
  { "breed": "Chow Chow", "name": "Butter"},
  { "breed": "Dachshund", "name": "Sousage"},
  { "breed": "Pug", "name": "Potat"},
  { "breed": "Corgi", "name": "Plop"},
  { "breed": "Pomeranian", "name": "Floof"}
]

const grid = useRef(null)
const textarea = useRef(null)

The JSX for this component creates the ZingGrid, textarea, and a button, binding the references to the grid and textarea and specifying the handler for the button:

zinggrid-react-demo/src/components/Methods.js
<div className="GridSidebar-wrapper">
  <zing-grid 
    ref={grid} 
    editor 
    pager
    head-class="grid-header"
    />
  <div className="Sidebar-wrapper">
    <br/>
    <textarea ref={textarea} cols="50" rows="12" spellCheck="false"></textarea>
    <br/>
    <button onClick={setData}>Set Data</button>
  </div>
</div>   

The work is done by the setData function, which gets the text from the textarea and sets it as the table data. setData is called both when the component is mounted, via the useEffect hook, and every time the button is pressed. The data object is edited in the textarea as JSON, which the corresponding ZingGrid API method setData accepts.

zinggrid-react-demo/src/components/Methods.js
const setData = () => grid.current.setData(textarea.current.value)

Events (zinggrid-react-demo/src/components/Events.js)

ZingGrid supports a set of events that are generated when the grid is rendered or the user interacts with it. This demo shows handling these events and logging them to a textarea.

We create two React references, grid and eventLog, and declare an array of the names of the events we want to monitor:

zinggrid-react-demo/src/components/Events.js
const grid = useRef(null)
const eventLog = useRef(null)
...
const events = ['grid:ready', 'cell:click', 'header:click', 'grid:pagechange']

In order to establish the event handler, we need to wait until the grid has been rendered. We do this using React's useEffect hook:

zinggrid-react-demo/src/components/Events.js
useEffect(() => {
  for(const ev of events)
    grid.current.addEventListener(ev, handleEvent)
})

Each of the events is bound to the same event listener:

zinggrid-react-demo/src/components/Events.js
const handleEvent = (event) => {
  let text = ''
  const target = event.detail.ZGTarget ? event.detail.ZGTarget.localName : 'zing-grid'

  text += `${event.type} from ${target}`
  text += details(event.type, event.detail.ZGData)
  text += '\n\n'

  eventLog.current.value += text
  eventLog.current.scrollTop = eventLog.current.scrollHeight
}

handleEvent receives an event object as its argument. We pick out the properties we'd like to display, mostly from event.detail, append them to the eventLog text, and scroll to the bottom of the textarea.

Data from REST Server (zinggrid-react-demo/src/components/Fetch.js)

Up to now we've been displaying grids with static data stored in a JavaScript object. In this demo we'll use JavaScript's fetch API to retrieve data dynamically. We'll be fetching from a JSON file on the application's server, but we could also fetch from a remote REST or GraphQL endpoint.

We again use a React reference to the grid and React's useEffect hook to set the data for the grid after it has been displayed:

zinggrid-react-demo/src/components/Fetch.js
// Fetch the dataset
async function getData() {
  try {
    const res = await fetch('./shows.json')
    const data = await res.json()
    // purposely timeout so the loading screen displays longer
    await delay(2000)
    grid.current.setData(data.shows)
  } catch(err) {
    console.log(err)
  }
}

// Call getData() on mount
useEffect(() => getData())

useEffect won't accept an asynchronous function but it will accept a synchronous function that calls an asynchronous function, so the fetch logic is moved into a separate function, getData. Here we await a fetch of the contents of the file ./shows.json from the server and convert the result to JSON. We insert a 2-second delay to simulate fetching from a remote server, and then use ZingGrid's setData API call to set the grid's data. The ZingGrid is configured to display a loading message by using the loading and loading-text attributes:

zinggrid-react-demo/src/components/Fetch.js
<zing-grid ref={grid}
  caption="Shows" 
  editor
  head-class="grid-header"
  loading
  loading-text="Loading (delayed by 2 seconds)">
</zing-grid>

Read Data from GraphQL Server (zinggrid-react-demo/src/components/GraphQL-read.js)

In the previous demo we fetched data using JavaScript, but ZingGrid can also be configured to fetch data itself from several types of servers (REST, GraphQL, Firebase, and others) using the zg-data web component. In this demo we'll read data from a public GraphQL endpoint.

Here is how the zing-grid component is configured:

zinggrid-react-demo/src/components/GraphQL-read.js
<zing-grid caption="GraphQL Demo" head-class="grid-header" loading>
  <zg-data src="https://countries.trevorblades.com/" adapter="graphql">
    <zg-param name="recordPath" value="data.continents"></zg-param>
    <zg-param name="readBody" value={readBodyJSON}></zg-param>
  </zg-data>
</zing-grid>

zg-data's src and adapter attributes specify that we're reading from a GraphQL server and set its URL. The contents of the zg-data component consist of zg-param components that control how the data is obtained. The readBody parameter's value is a JSON encoding of an object with a query property containing the GraphQL query:

zinggrid-react-demo/src/components/GraphQL-read.js
const readBodyJSON =  JSON.stringify({
  query: `
    {
      continents {
        name
      }
    }
  `
})

This specifies the structure of the data we want to receive. The resulting JSON object will look like this:

{
    "data": {
        "continents": [
            {"name": "Africa"},
            {"name": "Antarctica"},
            {"name": "Asia"},
            {"name": "Europe"},
            {"name": "North America"},
            {"name": "Oceania"},
            {"name": "South America"}
        ]
    }
}

The recordPath zg-param configures ZingGrid to use the data.continents property of the retrieved object as the tabular data for the grid.

GraphQL CRUD (zinggrid-react-demo/src/components/GraphQL-crud.js)

ZingGrid can do more than just read data from a remote source, it can be configured to allow editing of the table data and support full CRUD (Create/Read/Update/Delete) access to the data source.

To support this demo, ZingSoft hosts a GraphQL server at this URL. The data there is reset once an hour:

zinggrid-react-demo/src/components/GraphQL-crud.js
const demoServer = 'https://examples.zingsoft.com/graphql'

Here's the definition of the ZingGrid component:

zinggrid-react-demo/src/components/GraphQL-crud.js
<zing-grid context-menu caption="GraphQL CRUD Demo" head-class="grid-header" editor-controls>
  <zg-colgroup>
    <zg-column index="id" hidden editor="disabled"></zg-column>
    <zg-column index="title" header="Series Title" renderer="xss"></zg-column>
    <zg-column index="seasons" header="# of Seasons" type="number"></zg-column>
    <zg-column index="provider" header="Provider or Channel" renderer="xss"></zg-column>
    <zg-column index="genre" header="Genre" renderer="xss"></zg-column>
  </zg-colgroup>
  <zg-data src={demoServer} adapter="graphql">
    <zg-param name="recordPath" value="data.shows"></zg-param>
    <zg-param name="readBody" value={readBodyJSON}></zg-param>
    <zg-param name="createBody" value={createBodyJSON}></zg-param>
    <zg-param name="updateRowBody" value={updateRowBodyJSON}></zg-param>
    <zg-param name="updateCellBody" value={updateCellBodyJSON}></zg-param>
    <zg-param name="deleteBody" value={deleteBodyJSON}></zg-param>
  </zg-data>
</zing-grid>

First, note the addition of the editor-controls and context-menu attributes on the zing-grid component. editor-controls enables controls on the table for deleting and editing rows and adding new rows. context-menu enables a right-click popup menu that also includes some editing controls.

Skipping down to the zg-data sub-component we see recordPath and readBody parameters similar to the ones from the read-only GraphQL demo. The query for reading the data is:

zinggrid-react-demo/src/components/GraphQL-crud.js
const readBodyJSON = JSON.stringify({
  query: `
    query {
      shows {
        id
        title
        seasons
        provider
        genre
      }
    }`
  })

This will return a table of TV shows consisting of a title, number of seasons, provider, and genre for each. We also include the database id for each entry, but we do not want to display this to the user (we certainly don't want to allow the user to edit the ids!). In the zg-colgroup component of the grid we include a zg-column for the id, but add the hidden attribute so that the column does not appear in the grid, and the editor="disabled" attribute so that, when the user adds a new row to the table, the id field is not included in the row editor.

The id becomes important for the GraphQL mutations to update and delete data, specified as the zg-params updateRowBody, updateCellBody, and deleteBody queries. Let's look at the mutation for updating a row in the database (the same mutation is used for both updateRowBody and updateCellBody):

zinggrid-react-demo/src/components/GraphQL-crud.js
const updateRowBodyJSON = JSON.stringify({
  query: `
    mutation {
      updateShow(id:[[record.id]], title:"[[record.title]]", seasons:[[record.seasons]], provider:"[[record.provider]]", genre:"[[record.genre]]") {
        id
      }
    }`
  })

The updateShow mutation is provided by the GraphQL server and requires the id for the database row we want to modify, along with the new title, seasons, provider, and genre values. The [[record.*]] notations in the query text are template markers that will be filled in by ZingGrid before the query is sent to the GraphQL server. All of the fields of the edited row of the grid are available, prefixed by record..

Similarly, when we create a new row in the grid, the values entered into the popup form are available via [[record.*]] template markers and used to construct the createShow GraphQL mutation:

zinggrid-react-demo/src/components/GraphQL-crud.js
const createBodyJSON = JSON.stringify({ 
  query: `
    mutation {
      createShow(title:"[[record.title]]", seasons:[[record.seasons]], provider:"[[record.provider]]", genre:"[[record.genre]]") {
        id
      }
    }`
  })

Since the data is stored on a public server and could be vulnerable to cross-site scripting (XSS) attacks, we want to sanitize the data that's displayed. We obtain a filter function from the xss npm package:

zinggrid-react-demo/src/components/GraphQL-crud.js
import {filterXSS} from 'xss'

and register this method with ZingGrid, after the grid has been displayed, so we can use it in the component:

zinggrid-react-demo/src/components/GraphQL-crud.js
useEffect(() => ZingGrid.registerMethod(filterXSS, 'xss'))

In each of the zg-columns that contain text, we use the renderer attribute to cause the text to be filtered by filterXSS before it is displayed (for more on this technique, see the discussion of the Register Method demo below):

zinggrid-react-demo/src/components/GraphQL-crud.js
 ...
  <zg-colgroup>
    <zg-column index="id" hidden editor="disabled"></zg-column>
    <zg-column index="title" header="Series Title" renderer="xss"></zg-column>
    <zg-column index="seasons" header="# of Seasons" type="number"></zg-column>
    <zg-column index="provider" header="Provider or Channel" renderer="xss"></zg-column>
    <zg-column index="genre" header="Genre" renderer="xss"></zg-column>
  </zg-colgroup>
  ...

One-Way Binding (zinggrid-react-demo/src/components/OneWay.js)

This demo shows one-way binding from some HTML components to a ZingGrid. The bindings are managed via React state hooks, so no reference to the grid is required.

The definition of the grid incorporates several state variables:

zinggrid-react-demo/src/components/OneWay.js
<zing-grid 
  caption={caption}
  data={stringData}
  pager={pagerOn}
  theme={theme}
  head-class="grid-header"
  page-size="5"
  page-size-options="2,5,15,25,50" />

The rest of the JSX for this component consists of a textarea and other controls that modify the state. Each modification of state causes the grid to be re-rendered. The Load Data Set menu selects between the two datasets: an array of arrays of numbers and an array of objects describing some dog breeds. Note that, since we're setting the grid data using the data attribute, we must JSON encode the data.

Two-Way Binding (zinggrid-react-demo/src/components/TwoWay.js)

The two-way binding demo combines the use of React state as seen in the one-way binding demo with ZingGrid event handling to connect a grid to a textarea so that changes in either affect the other.

Let's look at the JSX for this component:

zinggrid-react-demo/src/components/TwoWay.js

The ZingGrid component definition includes a number of attributes that enable editing controls, the search mechanism, paging, and sorting. The data from the grid is a JSON rendering of the React state tableData. The value for the textarea is the textData state, which holds a JSON rendering of tableData. The onChange handler for the textarea updates both the tableData and textData states. Updating tableData causes the grid to be re-rendered with the new contents.

The useEffect hook establishes the event handler for each of the editing events that can occur on the grid:

zinggrid-react-demo/src/components/TwoWay.js
useEffect(() => {
  grid.current.addEventListener('data:record:delete', dataChange)
  grid.current.addEventListener('data:record:insert', dataChange)
  grid.current.addEventListener('data:cell:change', dataChange)
  grid.current.addEventListener('data:record:change', dataChange)
})

The dataChange function receives an event object containing the grid data and sets the textData state, updating the textArea:

zinggrid-react-demo/src/components/TwoWay.js
const dataChange = (ev) => {
  setTextData(JSON.stringify(ev.target.data))
}

By using two different state variables, one for the grid and one for the textarea, we avoid a loop where editing the grid triggers a change in the textarea, when then triggers a change to the grid, which triggers another update to the textarea, and so on.

Conditional Rendering (zinggrid-react-demo/src/components/Conditional.js)

In this demo we see how a ZingGrid can be used to show multiple datasets. A React state variable defaultColumns is used in multiple places in the grid definition to select between two different datasets and sets of column definitions:

zinggrid-react-demo/src/components/Conditional.js
<div className="Grid-wrapper">
  <button onClick={() => setDefaultColumns(!defaultColumns)}>
    Switch to
    {defaultColumns ? " Second " : " First "}
    Dataset
  </button>
  <zing-grid
    data={defaultColumns ? data1 : data2}
    caption={defaultColumns ? "Shows 1" : "Shows 2"}
    editor
    loading
    layout="row"
    head-class="grid-header"
    viewport-stop
    pager
    page-size="5"
  >
    {defaultColumns ? (
      <zg-colgroup>
        <zg-column index="title" />
        <zg-column index="genre" />
      </zg-colgroup>
    ) : (
      <zg-colgroup>
        <zg-column index="titleChanged" />
        <zg-column index="genreChanged" />
      </zg-colgroup>
    )}
  </zing-grid>
</div>

A button toggles the value of defaultColumns, causing the grid to be re-rendered with the alternate data and columns.

Register Method (zinggrid-react-demo/src/components/Register.js)

This final demo focuses on registering a method with ZingGrid to be used to change how data is rendered in the grid. We touched on this briefly, above, in the GraphQL/CRUD demo, where we used a render method to filter out possible cross-site scripting attacks.

By specifying the renderer attribute in a zg-column definition:

zinggrid-react-demo/src/components/Register.js
<zing-grid
  ref={grid}
  head-class="grid-header"
  caption="Hello World">
  <zg-colgroup>
    <zg-column index="first" header="Column 1" renderer="customRenderer"></zg-column>
    <zg-column index="last" header="Column 2"></zg-column>
    <zg-column index="number" header="Column 3"></zg-column>
  </zg-colgroup>
</zing-grid>

we can change how the data in that column is displayed to the user. The data itself is not modified, but each value is passed as the argument to the renderer function, and the function's return value is what is shown to the user.

In order to make the renderer function available to ZingGrid, we register it with registerMethod. The first argument is the function itself, the second argument is the name of the function as it appears in the renderer attribute in the grid definition:

zinggrid-react-demo/src/components/Register.js
useEffect(() => {
  grid.current.setData(dataSet)
  ZingGrid.registerMethod(customRenderer, 'customRenderer')
})

In this case the name of the function and the name we register it as are the same, but this is not required. The registerMethod call is executed within a useEffect hook, after the grid has been rendered.

The customRenderer function is simple, it just converts its input to uppercase:

zinggrid-react-demo/src/components/Register.js
const customRenderer = (text) => text.toUpperCase()

but any transformation can be performed.

Conclusion

I hope this article has provided a jumpstart in using ZingGrid in a React application. For more information, please visit the ZingGrid documentation for React at https://www.zinggrid.com/docs/integrations/js-frameworks-&-libs/react.

Interested in learning more about ZingGrid or trying it out for yourself? Let us know at support@zinggrid.com or visit us at https://www.zinggrid.com and chat directly with our team!

comments powered by Disqus