Using ZingGrid with Svelte 3

ZingGrid is a feature-rich JavaScript library for visualizing and interacting with tabular data. In this article we'll see how to get started with ZinGrid and Svelte and look at tapping into some ZingGrid features from within a Svelte app. This is not meant to be an exhaustive review of ZingGrid's capabilities, to learn more about what ZingGrid can do visit the ZingGrid home page. The GitHub repository corresponding with this article is here.

ZingGrid is written in vanilla JavaScript and can be used in any web application, with or without a framework. Since it's implemented as a standard web component, no wrapper is needed to incorporate it into a Svelte app. We'll start out with Svelte template and quickly get a static grid displayed. Then we'll look at each of the pages in the demo and highlight how to integrate ZingGrid features like themes, events, and data binding within a Svelte context. We won't build the entire demo app in this article, so I recommend that you download the demo and start it up as a reference.

Hello World

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

npx degit sveltejs/template zinggrid-svelte-helloworld
cd zinggrid-svelte-helloworld
npm install
npm install zinggrid
npm run dev

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

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

Let's start by adding the import for ZingGrid. We'll also remove the name component property:

zinggrid-svelte-helloworld/src/App.svelte
<script>
  import 'zinggrid'
</script>

Now remove the contents of <main> and replace them with a zing-grid component:

zinggrid-svelte-helloworld/src/App.svelte
<main>
  <zing-grid />
</main>

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. In the <script> section, 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-svelte-helloworld/src/App.svelte
<script>
  import 'zinggrid'

  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"}
  ]
</script>

Change the zing-grid component to look like this:

zinggrid-svelte-helloworld/src/App.svelte
<main>
  <zing-grid caption="Hello Futurama" data={JSON.stringify(data)} />
</main>

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 the <script> section).

You should now see a grid in the browser! Note that the text in the grid is centered, this is because of the text-align: center; CSS styling applied to <main> in the <style> section. We could fix this by deleting that line, but, since ZingGrid is a standard web component, we can just style it with CSS:

zinggrid-svelte-helloworld/src/App.svelte
<style>
  ...
  zing-grid {
    text-align: left;
  }
  ...
</style>

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 Svelte app.

zinggrid-svelte-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 Svelte and ZingGrid.

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

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

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

Hello World (zinggrid-svelte-demo/src/demos/Simple.svelte)

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

  1. We've added the head-class attribute to the zing-grid component. This attaches the CSS class grid-header (defined in zinggrid-svelte-demo/public/global.css) to the header, making it appear in a bold font.
  2. The component is no longer wrapped in a <main> component, it's wrapped in a <div> that's styled in the global.css file.

Themes (zinggrid-svelte-demo/src/demos/Themes.svelte)

This demo shows how to use a Svelte 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 declaring a component variable grid to hold the reference and another, theme, to hold the grid's theme. We then declare the table data, in this case as an array of arrays, one per row:

zinggrid-svelte-demo/src/demos/Themes.svelte
let grid
let theme = 'default'

const data = [
  [1,2,3],
  [4,5,6]
]

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

zinggrid-svelte-demo/src/demos/Themes.svelte
<zing-grid bind:this={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 bind:this 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 reference. In this case we use Svelte's onMount function to invoke grid.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-svelte-demo/src/demos/Themes.svelte
<select value={theme} on:change={ev => theme = 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 on:change handler sets theme to the selected theme name, which causes Svelte to rerender the grid with the chosen theme.

Methods (zinggrid-svelte-demo/src/demos/Methods.svelte)

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 two Svelte references for the grid and textarea and the initial table data. Note that this data array consists of objects, each of which has breed and name properties.

zinggrid-svelte-demo/src/demos/Methods.svelte
let grid = null
let textarea = null

const dataSet = [
  { "breed": "Chow Chow", "name": "Butter"},
  { "breed": "Dachshund", "name": "Sousage"},
  { "breed": "Pug", "name": "Potat"},
  { "breed": "Corgi", "name": "Plop"},
  { "breed": "Pomeranian", "name": "Floof"}
]

The HTML 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-svelte-demo/src/demos/Methods.svelte
<div class="GridSidebar-wrapper">
  <zing-grid 
    bind:this={grid} 
    editor 
    pager={true}
    head-class="grid-header"
    />
  <div class="Sidebar-wrapper">
    <br/>
    <textarea bind:this={textarea} cols="50" rows="12" spellcheck="false"></textarea>
    <br/>
    <button on:click={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 onMount, 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-svelte-demo/src/demos/Methods.svelte
const setData = () => grid.setData(textarea.value)

Events (zinggrid-svelte-demo/src/demos/Events.svelte)

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 Svelte references, grid and eventLog, and declare an array of the names of the events we want to monitor:

zinggrid-svelte-demo/src/demos/Events.svelte
let grid = null
let eventLog = 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 Svelte's onMount function:

zinggrid-svelte-demo/src/demos/Events.svelte
onMount(() => {
  for(const ev of events)
    grid.addEventListener(ev, handleEvent)
})

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

zinggrid-svelte-demo/src/demos/Events.svelte
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.value += text
  eventLog.scrollTop = eventLog.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-svelte-demo/src/demos/Fetch.svelte)

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 Svelte reference to the grid and Svelte's onMount function to set the data for the grid after it has been displayed:

zinggrid-svelte-demo/src/demos/Fetch.svelte
// 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.setData(data.shows)
  } catch(err) {
    console.log(err)
  }
}

onMount(getData)

The fetch logic is moved into a separate function, getData, called via onMount. 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-svelte-demo/src/demos/Fetch.svelte
<zing-grid bind:this={grid}
  caption="Shows" 
  editor
  head-class="grid-header"
  loading={true}
  loading-text="Loading (delayed by 2 seconds)">
</zing-grid>

Read Data from GraphQL Server (zinggrid-svelte-demo/src/demos/GraphQL-read.svelte)

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-svelte-demo/src/demos/GraphQL-read.svelte
<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-svelte-demo/src/demos/GraphQL-read.svelte
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-svelte-demo/src/demos/GraphQL-crud.svelte)

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-svelte-demo/src/demos/GraphQL-crud.svelte
const demoServer = 'https://examples.zingsoft.com/graphql'

Here's the definition of the ZingGrid component:

zinggrid-svelte-demo/src/demos/GraphQL-crud.svelte
<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-svelte-demo/src/demos/GraphQL-crud.svelte
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-svelte-demo/src/demos/GraphQL-crud.svelte
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-svelte-demo/src/demos/GraphQL-crud.svelte
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-svelte-demo/src/demos/GraphQL-crud.svelte
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-svelte-demo/src/demos/GraphQL-crud.svelte
onMount(() => 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-svelte-demo/src/demos/GraphQL-crud.svelte
 ...
  <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-svelte-demo/src/demos/OneWay.svelte)

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

The definition of the grid incorporates several component variables:

zinggrid-svelte-demo/src/demos/OneWay.svelte
<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 HTML 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-svelte-demo/src/demos/TwoWay.svelte)

The two-way binding demo combines the use of Svelte component variables 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 HTML for this component:

zinggrid-svelte-demo/src/demos/TwoWay.svelte
<div class="GridSidebar-wrapper">
  <zing-grid 
    bind:this={grid}
    caption="Two-Way Data Binding (right click on cells for context menu options)"
    context-menu
    editor
    editor-controls
    layout-controls
    search
    sorter
    pager
    page-size="5"
    head-class="grid-header"
    page-size-options="2,5,15"
    data={JSON.stringify(tableData)} />
  <div class="Sidebar-wrapper">
    <p>JSON data</p>
    <textarea name="ds" cols="50" rows="22" value={textData} spellCheck="false"
      on:input={ev => { tableData = JSON.parse(ev.target.value); textData = ev.target.value } } />
  </div>
</div>

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 Svelte variable tableData. The value for the textarea is the textData variable, which holds a JSON rendering of tableData. The on:change 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 onMount function establishes the event handler for each of the editing events that can occur on the grid:

zinggrid-svelte-demo/src/demos/TwoWay.svelte
onMount(() => {
  grid.addEventListener('data:record:delete', dataChange)
  grid.addEventListener('data:record:insert', dataChange)
  grid.addEventListener('data:cell:change', dataChange)
  grid.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-svelte-demo/src/demos/TwoWay.svelte
const dataChange = (ev) => {
  textData = JSON.stringify(ev.target.data)
}

By using two different component 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-svelte-demo/src/demos/Conditional.svelte)

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

zinggrid-svelte-demo/src/demos/Conditional.svelte
<div class="Grid-wrapper">
  <button on:click={() => defaultColumns = !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={true}
    page-size="5"
  >
    {#if defaultColumns}
      <zg-colgroup>
        <zg-column index="title" />
        <zg-column index="genre" />
      </zg-colgroup>
    {:else}
      <zg-colgroup>
        <zg-column index="titleChanged" />
        <zg-column index="genreChanged" />
      </zg-colgroup>
    {/if}
  </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-svelte-demo/src/demos/Register.svelte)

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-svelte-demo/src/demos/Register.svelte
<zing-grid
  bind:this={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-svelte-demo/src/demos/Register.svelte
onMount(() => {
  grid.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 onMount, after the grid has been rendered.

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

zinggrid-svelte-demo/src/demos/Register.svelte
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 Svelte application. For more information, please visit the ZingGrid documentation at https://www.zinggrid.com/docs.

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!