Using ZingGrid with React 17
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:
- The component name has been changed from
App
toSimple
. - We use React's
useState
hook to define the data array, even though we're not modifying it yet. - We've added the
head-class
attribute to thezing-grid
component. This attaches the CSS classgrid-header
(defined inzinggrid-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-param
s 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-column
s 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!