{"id":21448704,"url":"https://github.com/rkristelijn/dhtmlx-json-node","last_synced_at":"2026-04-19T14:33:58.647Z","repository":{"id":98730928,"uuid":"134526757","full_name":"rkristelijn/dhtmlx-json-node","owner":"rkristelijn","description":"This is a short tutorial to set up dhtmlx (dhx) using only JSON and node/mongodb as a back-end and the REST API. Recently I followed the 'Your First App' tutorial and felt unsatisfied with what I've learned. So I decided to push a little harder and use a demo app 'CRM System' as the base and create my own tutorial.","archived":false,"fork":false,"pushed_at":"2018-06-20T11:06:32.000Z","size":3217,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-03-17T02:11:37.806Z","etag":null,"topics":["crm","dhtmlx","dhx","json","node","tutorial"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rkristelijn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-05-23T06:54:36.000Z","updated_at":"2018-06-20T11:06:34.000Z","dependencies_parsed_at":"2023-05-25T01:30:44.794Z","dependency_job_id":null,"html_url":"https://github.com/rkristelijn/dhtmlx-json-node","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rkristelijn/dhtmlx-json-node","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rkristelijn%2Fdhtmlx-json-node","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rkristelijn%2Fdhtmlx-json-node/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rkristelijn%2Fdhtmlx-json-node/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rkristelijn%2Fdhtmlx-json-node/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rkristelijn","download_url":"https://codeload.github.com/rkristelijn/dhtmlx-json-node/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rkristelijn%2Fdhtmlx-json-node/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269734149,"owners_count":24466554,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-10T02:00:08.965Z","response_time":71,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crm","dhtmlx","dhx","json","node","tutorial"],"created_at":"2024-11-23T03:16:23.521Z","updated_at":"2026-04-19T14:33:58.597Z","avatar_url":"https://github.com/rkristelijn.png","language":"JavaScript","readme":"# dhtmlx-json-node\n\n## Introduction\n\nThis is a short tutorial to set up dhtmlx (dhx) using only JSON and node/mongodb as a back-end and the REST API. Recently I followed the ['Your First App'](https://docs.dhtmlx.com/tutorials__first_app__index.html) tutorial and felt unsatisfied with what I've learned. So I decided to push a little harder and use a demo app ['CRM System'](https://dhtmlx.com/docs/products/demoApps/dhtmlxCRMSystem/index.html) as the base and create my own tutorial.\n\nUpdate: 20-JUN-2018: this project focuses on getting the framework up and running, interaction with different components and creating and connecting to the back-end. It doesn't concern optimizing the front-end in terms of code overview, readeability, systainability for changes and so on, but [this project does](https://github.com/rkristelijn/optimus-start) using [DHTMLX.com](https://docs.dhtmlx.com/optimus__index.html)\n\n# [DHTMLX Optimus Framework](https://docs.dhtmlx.com/optimus__index.html)\n\nDHTMLX Optimus is a micro framework for building DHTMLX-based apps.\nThe framework enforces consistent application structure by breaking a monolithic application into a set of reusable classes. As a result, each part of an app can be developed and tested independently and used in various combinations.\nDHTMLX Optimus is a fully client-side solution. There aren't any special requirements to the server. You can use any data REST backend (PHP, Nodejs, .Net, Java, etc.) \n\nThis project only makes the proposed ['CRM System'] demo(https://dhtmlx.com/docs/products/demoApps/dhtmlxCRMSystem/index.html) work end-to-end, using fetch, and a REST API using Node.\n\n# Contents\n- [Motivational speech](#why)\n- [Screenshots](#example)\n- [Comparison](#benchmark)\n- [Installation](#installation)\n- [Tutorial](#tutorial)\n\n---\n\n# Why?\n\n[back to Contents](#contents)\n\nWhy you ask? DHTMLX (dhx for short) is a beautiful library to quickly ramp up CMS, CRM, HR, etc systems that are intuitive and complete. Probably you can use alternatives such as angular/vue/react material, but that comes with a full framework. This time I just wanted the simplicity of dhx itself. \n\n## [DHTMLX](https://docs.dhtmlx.com/index.html) API references:\n\n### Layout \n\n|[Accordion](https://docs.dhtmlx.com/api__refs__dhtmlxaccordion.html)| [Carousel](https://docs.dhtmlx.com/api__refs__dhtmlxcarousel.html) | [Layout](https://docs.dhtmlx.com/api__refs__dhtmlxlayout.html) | [Popup](https://docs.dhtmlx.com/api__refs__dhtmlxpopup.html) | [Tabbar](https://docs.dhtmlx.com/api__refs__dhtmlxtabbar.html) | [Windows](https://docs.dhtmlx.com/api__refs__dhtmlxwindows.html) |\n|:-:|:-:|:-:|:-:|:-:|:-:|\n|[![icon](/tutorial_images/dhx_icons/icon_accordion.png)](https://docs.dhtmlx.com/api__refs__dhtmlxaccordion.html)|[![icon](/tutorial_images/dhx_icons/icon_carousel.png)](https://docs.dhtmlx.com/api__refs__dhtmlxcarousel.html)|[![icon](/tutorial_images/dhx_icons/icon_layout.png)](https://docs.dhtmlx.com/api__refs__dhtmlxlayout.html) |[![icon](/tutorial_images/dhx_icons/icon_popup.png)](https://docs.dhtmlx.com/api__refs__dhtmlxpopup.html)|[![icon](/tutorial_images/dhx_icons/icon_tabbar.png)](https://docs.dhtmlx.com/api__refs__dhtmlxtabbar.html) |[![icon](/tutorial_images/dhx_icons/icon_windows.png)](https://docs.dhtmlx.com/api__refs__dhtmlxwindows.html) \n\n### Data Components \n\n|[Chart](https://docs.dhtmlx.com/api__refs__dhtmlchart.html)| [DataView](https://docs.dhtmlx.com/api__refs__dhtmlxdataview.html) | [Grid](https://docs.dhtmlx.com/api__refs__dhtmlxgrid.html) | [List](https://docs.dhtmlx.com/api__refs__dhtmlxlist.html) | [TreeGrid](https://docs.dhtmlx.com/api__refs__dhtmlxtreegrid.html) | [TreeView](https://docs.dhtmlx.com/api__refs__dhtmlxtreeview.html) |\n|:-:|:-:|:-:|:-:|:-:|:-:|\n|[![icon](/tutorial_images/dhx_icons/icon_chart.png)](https://docs.dhtmlx.com/api__refs__dhtmlxchart.html)|[![icon](/tutorial_images/dhx_icons/icon_dataview.png)](https://docs.dhtmlx.com/api__refs__dhtmlxdataview.html)|[![icon](/tutorial_images/dhx_icons/icon_grid.png)](https://docs.dhtmlx.com/api__refs__dhtmlxgrid.html) |[![icon](/tutorial_images/dhx_icons/icon_list.png)](https://docs.dhtmlx.com/api__refs__dhtmlxlist.html)|[![icon](/tutorial_images/dhx_icons/icon_treegrid.png)](https://docs.dhtmlx.com/api__refs__dhtmlxtreegrid.html) |[![icon](/tutorial_images/dhx_icons/icon_treeview.png)](https://docs.dhtmlx.com/api__refs__dhtmlxtreeview.html) \n\n### Form-oriented Components \n\n| [Calendar](https://docs.dhtmlx.com/api__refs__dhtmlxcalendar.html)| [ColorPicker](https://docs.dhtmlx.com/api__refs__dhtmlxcolorpicker.html) | [Combo](https://docs.dhtmlx.com/api__refs__dhtmlxcombo.html) | [Editor](https://docs.dhtmlx.com/api__refs__dhtmlxeditor.html) | [Form](https://docs.dhtmlx.com/api__refs__dhtmlxform.html) | [Slider](https://docs.dhtmlx.com/api__refs__dhtmlxslider.html) \n|:-:|:-:|:-:|:-:|:-:|:-:|\n|[![icon](/tutorial_images/dhx_icons/icon_calendar.png)](https://docs.dhtmlx.com/api__refs__dhtmlxcalendar.html)|[![icon](/tutorial_images/dhx_icons/icon_colorpicker.png)](https://docs.dhtmlx.com/api__refs__dhtmlxcolorpicker.html)|[![icon](/tutorial_images/dhx_icons/icon_combo.png)](https://docs.dhtmlx.com/api__refs__dhtmlxcombo.html) |[![icon](/tutorial_images/dhx_icons/icon_editor.png)](https://docs.dhtmlx.com/api__refs__dhtmlxeditor.html)|[![icon](/tutorial_images/dhx_icons/icon_form.png)](https://docs.dhtmlx.com/api__refs__dhtmlxform.html) |[![icon](/tutorial_images/dhx_icons/icon_slider.png)](https://docs.dhtmlx.com/api__refs__dhtmlxslider.html) |\n\n### Navigation Components\n\n| [Menu](https://docs.dhtmlx.com/api__refs__dhtmlxmenu.html)| [Ribbon](https://docs.dhtmlx.com/api__refs__dhtmlxribbon.html) | [Sidebar](https://docs.dhtmlx.com/api__refs__dhtmlxsidebar.html) | [Toolbar](https://docs.dhtmlx.com/api__refs__dhtmlxtoolbar.html) |\n|:-:|:-:|:-:|:-:|\n|[![icon](/tutorial_images/dhx_icons/icon_menu.png)](https://docs.dhtmlx.com/api__refs__dhtmlxmenu.html)|[![icon](/tutorial_images/dhx_icons/icon_ribbon.png)](https://docs.dhtmlx.com/api__refs__dhtmlxribbon.html)|[![icon](/tutorial_images/dhx_icons/icon_sidebar.png)](https://docs.dhtmlx.com/api__refs__dhtmlxsidebar.html) |[![icon](/tutorial_images/dhx_icons/icon_toolbar.png)](https://docs.dhtmlx.com/api__refs__dhtmlxtoolbar.html)\n\nWhen connecting the back-end to it (in English: make the new, delete button work, and allow updates that stay) I discovered that the default dhtmlxDataProcessor is unpredictable and heavy to use. [@see my other project](https://github.com/rkristelijn/grid-socket-io)\n\nWhen sending data from the browser, these are the options you have:\n\n- [XMLHttpRequest or XHR](https://blog.garstasio.com/you-dont-need-jquery/ajax/) - old [AJAX (Asynchronous JavaScript And XML)](https://en.wikipedia.org/wiki/Ajax_(programming))\n- [jQuery](https://blog.garstasio.com/you-dont-need-jquery/ajax/)\n- [JSONP](https://en.wikipedia.org/wiki/JSONP)\n- [WebSocket](https://en.wikipedia.org/wiki/WebSocket)\n- [fetch polyfil](https://github.com/github/fetch)\n- [xdomain](https://github.com/jpillora/xdomain)\n\nBut now, the replacement of XMLHttpRequest is here: **fetch** and [you can use it out of the box](https://caniuse.com/#feat=fetch)\n\nSo, this example uses: \n- [fetch](https://developers.google.com/web/ilt/pwa/working-with-the-fetch-api) (instead of [dhtmlxDataProcessor](https://docs.dhtmlx.com/dataprocessor__index.html) based on [XMLHttpRequest](https://blog.garstasio.com/you-dont-need-jquery/ajax/)) - make it ready for streaming technology also look at [Jake Archibald's comparison page](https://jakearchibald.com/2015/thats-so-fetch/)\n- [REST (Representational State Transfer](https://en.wikipedia.org/wiki/Representational_state_transfer) [API](https://en.wikipedia.org/wiki/Application_programming_interface) (instead [PHPConnector](https://docs.dhtmlx.com/connector__php__index.html)) leveraging HTTP using GET, POST, PUT, DELETE\n- [Node](https://en.wikipedia.org/wiki/Node.js) (Instead of [PHP](https://en.wikipedia.org/wiki/PHP)), \n- [JSON](https://en.wikipedia.org/wiki/JSON) (instead of [XML](https://en.wikipedia.org/wiki/XML))\n\n# Example\n\n[back to Contents](#contents)\n\nBelow you will find screenshots of the demo app ['CRM System'](https://dhtmlx.com/docs/products/demoApps/dhtmlxCRMSystem/index.html) only it runs from my [Raspberry Pi](https://en.wikipedia.org/wiki/Raspberry_Pi) (@see [tutorial to set up](https://github.com/rkristelijn/pi-ci-mean)).\n\n![Screenshot of dhtmlx with a working grid that changes also the form when you click a record](/tutorial_images/Screenshot_20180528_112120.png)\n\n![Screenshot of dhtmlx with the projects tile opened showing a grid, a graph and detail view](/tutorial_images/Screenshot_20180528_112132.png)\n\n![Screenshot of dhtmlx with the event tile opened, showing tiles of events](/tutorial_images/Screenshot_20180528_112143.png)\n\n![Screenshot of dhtmlx with the settings tile opened showing tiles and a form](/tutorial_images/Screenshot_20180528_112153.png)\n\n## Benchmark\n\n[back to Contents](#contents)\n\n### Sample application\n\n- 53 requests\n- 385 KB transferred\n- Finish 5.87s\n- [DOMContentLoaded](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded): 337ms\n- Load: 376\n- Location: Internet (San Francisco, California, United States)\n- Full-blown webserver I presume: nginx/1.1.19\n- Static content\n- Uses local dhtmlx.js and dhtmlx.css\n\n![Demo application loaded from cache where possible](/tutorial_images/Screenshot_20180613_131627.png)\n\n### This application\n\n- 34 requests (**17 less**)\n- 81.8 KB transferred (**4,7 times smaller**)\n- Finish 1.08s (**5 times faster**)\n- DOMContentLoaded: n/a\n- Load: n/a\n- Location: LAN\n- Raspberry Pi 3 using Express in Node\n- Data from MongoDB\n- Ability to Edit, Add and Delete\n- Uses [CDN](https://dhtmlx.com/blog/support-updates-dhtmlx-cdn-new-snippet-tool/) to get dhtmlx.js and dhtmlx.css\n\n![This application loading from cache where possible](/tutorial_images/Screenshot_20180613_131906.png)\n\n\n# Installation\n\n[back to Contents](#contents)\n\n1. `git clone https://github.com/rkristelijn/dhtmlx-json-node.git`\n2. `cd dhtmlx-json-node`\n3. `npm i`\n4. `npm run seed` / ctrl+c when done\n5. `npm start`\n6. point your browser at `localhost:3000`\n\n---\n\n# Tutorial\n\n[back to Contents](#contents)\n\nNow follows every single step I took in ramping up my app.\n\n## Prerequisites:\n\n1. latest node and npm\n2. mongodb server up and running\n3. up-to-date javascript, git, bash and mongodb skills\n\nYou could have a look at setting up such an environment on either a [physical raspberry pi](https://www.linkedin.com/pulse/develop-full-stack-javascript-applications-using-only-remi-kristelijn/) or a [virtual raspberry pi](https://www.linkedin.com/pulse/my-perfect-javascript-developer-set-up-remi-kristelijn/).\n\nIn this tutorial I consider: less is more, if we can use defaults, we should do it (default: index.html, index.js etc). Only use stuff if and when we need it. I try to follow the latest standards, like [lambdas](https://en.wikipedia.org/wiki/Anonymous_function), [HTML5](https://en.wikipedia.org/wiki/HTML5), [WCAG](https://en.wikipedia.org/wiki/Web_Content_Accessibility_Guidelines), [OWASP](https://en.wikipedia.org/wiki/OWASP), etc. Also I try to teach only once: the right way. I think it is better to learn things the right way and later discover; 'hey... there is also a old or wrong way'. \n\nThis tutorial follows my development step by step using git branches. Every chapter contains a link to the feature branch.\n\n# Plan\n\n[back to Contents](#contents)\n\n  - [x] [Step1: Create static html page in Node](#step-1-static-node-web-server)\n  - [x] [Step2: Get dhx up and running](#step-2-get-dhx-up-and-running)\n  - [x] [Step3: Initialize the layout, grid and form with static data](#step-3-initialize-the-layout-grid-and-form-with-static-data)\n    - [x] Step3a: Improve code\n      - [x] [Step3a1: Fix xml to json](#step-3-a-change-xml-to-json)\n    - [x] Use streamable technology\n  - [x] [Step4: Create and connect REST API](#step-4-create-and-connect-rest-api)\n    - [x] GET using statics for [contacts](#step-4-create-and-connect-rest-api), [projects](#step-4b-set-up-get-for-projects-with-a-static-file), [events and settings](#step-4c-events-and-settings).\n    - [x] [GET using data from db](#step-4d-get-using-data-from-db)\n    - [x] [PUT for updates](#step-4e-put-for-updates) for the grid and the from\n    - [x] [POST for create](#step-4d-post-for-create)\n    - [x] [DELETE for delete](#step-4f-delete-for-delete)\n\nNot part of tutorial\n\n  - [x] Connect other views [projects](https://github.com/rkristelijn/dhtmlx-json-node/tree/feature-projects-api), [events](https://github.com/rkristelijn/dhtmlx-json-node/tree/feature-events-api) and [settings](https://github.com/rkristelijn/dhtmlx-json-node/tree/feature-settings-api)\n  - [ ] Clean up code\n    - [ ] big global of the settingsforms can be taken out of the code\n  - [x] Optimize dataProcessor (moved to app.js and reused)\n  - [ ] Optimize front-end (vue.js?)\n    - [ ] Enable routes, so when pressing F5 just stays on the same view\n  - [ ] Extend features\n    - [ ] so sales, settings, events can be added, changed and deleted\n    - [ ] dates are datepickers\n    - [ ] multiselects, dropdowns\n  - [ ] implement error handling\n  - [References](#references)\n\n# Step 1: Static node web server\n\n[back to top](#plan)\n\n[@see branch step 2](https://github.com/rkristelijn/dhtmlx-json-node/tree/step1)\n\nUse npm to create a new `package.json` file:\n\n`npm init`\n- package name: (default)\n- version: 0.0.1\n- description: Basic application using dhtmlx, json, node, rest\n- entry point: index.js\n- test command: (empty)\n- git repo: (default)\n- keywords: dhtmlx, json, node, rest\n- author: Remi Kristelijn\n- licence: (default: ISC)\n- Ok?: yes\n\nBefore we start coding, we need express as our middleware.\n\n`npm i --save express`\n\nThis created the node_modules folder that we need to add to the .gitignore file:\n\n`echo node_modules \u003e .gitignore`\n\nThen we create the entry point `index.js`\n\n```javascript\nconst express = require('express');\nconst app = express();\n\napp.get('/', (req, res) =\u003e {\n  res.send(\"\u003ch1\u003eHello World\u003c/h1\u003e\");\n});\n\napp.listen(3000, () =\u003e {\n  console.log('listening on *:3000');\n});\n```\nThat is all you need to create a simple node app. You can either use `node .` to fire up your application and point your browser to `http://localhost:3000`\n\nLet's save our work using `git add.`, `git commit -a -m \"blablabla\"` and `git push`\n\n# Step 2: Get dhx up and running\n\n[back to top](#plan)\n\n[@see branch step 2](https://github.com/rkristelijn/dhtmlx-json-node/tree/step2))\n\nInstead of sending a string, we will send a static html page.\n\nA few notes; \n- as of August 2016 [dhx provides a CDN](https://dhtmlx.com/blog/support-updates-dhtmlx-cdn-new-snippet-tool/) so we don't have to download the javascript files.\n- WCAG dictates a page should have a language\n\nCreate `index.html`:\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"//cdn.dhtmlx.com/edge/dhtmlx.css\"\u003e\n  \u003cscript src=\"//cdn.dhtmlx.com/edge/dhtmlx.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\"\u003e\n    dhtmlxEvent(window, \"load\", function () {\n      dhtmlx.message({ type: \"alert\", text: \"Hello world\" });\n    });\n  \u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\nUpdate `index.js`:\n```javascript\nconst express = require('express');\nconst app = express();\n\napp.get('/', (req, res) =\u003e {\n  // step2: instead of sending a string, we send a file\n  res.sendFile(__dirname + '/index.html');\n});\n\napp.listen(3000, () =\u003e {\n  console.log('listening on *:3000');\n});\n```\nRestart your node application and enjoy the majesty.\n\n![Screenshot of dhtmlx with only a modal dialogue saying hello world](/tutorial_images/Screenshot_20180523_102030.png)\n\n# Step 3: Initialize the layout, grid and form with static data\n\n[back to top](#plan)\n\n[@see branch step 3](https://github.com/rkristelijn/dhtmlx-json-node/tree/step3))\n\nBest practices is to not mix html, javascript, css, etc together, so in this case, we are writing our dhtmlx app inline of index.html. Let's fix that. Before we do that, we need to create folder that is able to load our static files. We call this folder `public`.\n\n- `mkdir public`\n- `mv index.html public`\n\nUpdate `index.js`\n\n```javascript\nconst express = require('express');\nconst app = express();\nconst path = require('path'); // step 3: we need to add path in order to use it\n\napp.use('/', express.static(path.join(__dirname, 'public'))); // step 3: just serve up the full public directory\n\napp.listen(3000, () =\u003e {\n  console.log('listening on *:3000');\n});\n```\n- type `node .` just to make sure we didn't break anything.\n\n![Screenshot of dhtmlx with only a modal dialogue saying hello world](/tutorial_images/Screenshot_20180523_102030.png)\n\nNow it is time to extract the javascript from `index.html`.\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"//cdn.dhtmlx.com/edge/dhtmlx.css\"\u003e\n  \u003cscript src=\"//cdn.dhtmlx.com/edge/dhtmlx.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\" src=\"app.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\nCreate a new file `public/app.js`\n\n```javascript\ndhtmlxEvent(window, \"load\", function () {\n  dhtmlx.message({ type: \"alert\", text: \"Hello world\" });\n});\n```\n\nSave and restart the server\n\n![Screenshot of dhtmlx with only a modal dialogue saying hello world, network tab expanded showing app.js loading](/tutorial_images/Screenshot_20180523_104326.png)\n\nIsn't that sweet.\n\nBefore we continue we probably are tired of restarting the server. Let's fix that using nodemon.\n\n`npm i --save-dev nodemon`\n\nWe don't need to install globally as we can access the nodemon executables from the npm script.\n\nUpdate `package.json`, add a `start`-script and add an ignore for the public folder:\n\n```json\n{\n  \"//\u003c!--...\":\"...--\u003e\",\n\n  \"scripts\": {\n    \"start\": \"nodemon ./index.js --ignore public/\",\n    \"test\": \"echo \\\"Error: no test specified\\\" \u0026\u0026 exit 1\"\n  },\n\n  \"//\u003c!--...\":\"...--\u003e\",\n\n  \"dependencies\": {\n    \"express\": \"^4.16.3\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"^1.17.5\"\n  }\n}\n```\nNow we are going to create the sidebar and the menu. It seems the demo app ['CRM System'](https://dhtmlx.com/docs/products/demoApps/dhtmlxCRMSystem/index.html) uses a different skin.\n\nUpdate `index.html`:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"//cdn.dhtmlx.com/edge/skins/web/dhtmlx.css\"\u003e\n  \u003cscript src=\"//cdn.dhtmlx.com/edge/dhtmlx.js\"\u003e\u003c/script\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\" src=\"app.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\nAlso create `public/style.css`\n```css\nhtml, body {\n\twidth: 100%;\n\theight: 100%;\n\tmargin: 0px;\n\toverflow: hidden;\n}\n```\n\nThen we need to create all elements, update `public/app.js`\n\n```javascript\nvar mainSidebar;\nvar mainToolbar;\n\nfunction appInit() {\n  mainSidebar = new dhtmlXSideBar({\n    parent: document.body,\n    icons_path: \"imgs/sidebar/\",\n    width: 180,\n    template: \"tiles\",\n    items: [\n      { id: \"contacts\", text: \"Contacts\", icon: \"contacts.png\" },\n      { id: \"projects\", text: \"Projects\", icon: \"projects.png\" },\n      { id: \"events\", text: \"Events\", icon: \"events.png\" },\n      { id: \"settings\", text: \"Settings\", icon: \"settings.png\" }\n    ]\n  });\n\n  mainToolbar = mainSidebar.attachToolbar({\n    icons_size: 32,\n    icons_path: \"imgs/toolbar/\",\n    items: [\n      { type: \"text\", id: \"title\", text: \"\u0026nbsp;\" },\n      { type: \"spacer\" },\n      { type: \"button\", id: \"add\", img: \"add.png\" },\n      { type: \"button\", id: \"save\", img: \"save.png\" }\n    ]\n  });\n\n  mainSidebar.attachEvent(\"onSelect\", function (id) {\n    // todo, below doesn't look right; HTML in javascript\n    mainToolbar.setItemText(\"title\", window.dhx4.template(\"\u003cspan style='font-weight: bold; font-size: 14px;'\u003e#text#\u003c/span\u003e\", { text: mainSidebar.cells(id).getText().text }));\n  });\n\n  // select 'Contacts' by default\n  mainSidebar.cells(\"contacts\").setActive(true);\n}\n\ndhtmlxEvent(window, \"load\", appInit);\n\n```\n\nRefresh and this is what we get:\n\n![Screenshot of dhtmlx a sidebar with 4 items and a toolbar with two buttons](/tutorial_images/Screenshot_20180523_135554.png)\n\nNow the demo uses different js files for the 4 options in the menu and this is initialized on start. We combine the file to improve performance to have only one js file downloaded. Also the demo uses Google maps, we just ignore this feature for now as we need API keys in order for it to work. Also we need tons of images, data files etc. We combine this into one app. Next to that we need to remove the loader from the codebase.\n\nThe application looks now like this, and no console errors.\n\n![Screenshot of dhtmlx with a working grid that changes also the form when you click a record](/tutorial_images/Screenshot_20180528_112120.png)\n\n![Screenshot of dhtmlx with the projects tile opened showing a grid, a graph and detail view](/tutorial_images/Screenshot_20180528_112132.png)\n\n![Screenshot of dhtmlx with the event tile opened, showing tiles of events](/tutorial_images/Screenshot_20180528_112143.png)\n\n![Screenshot of dhtmlx with the settings tile opened showing tiles and a form](/tutorial_images/Screenshot_20180528_112153.png)\n\n## Step 3 a: Change XML to JSON\n\n[back to top](#plan)\n\n[@see branch Step3a1](https://github.com/rkristelijn/dhtmlx-json-node/tree/Step3a1)\n\nJSON is much more lightweight than XML. \n\n```bash\npi@raspberry:~/dhtmlx-json-node/public/server $ ls -al\n4364 contacts-minified.json\n4445 contacts-ndjson.json\n5639 contacts-minified.xml\n6475 contacts-fully-beautified.json\n6588 contacts-fully-beautified.xml\n```\n\nI'm only drawing conclusions on the minified versions, this is an improvement of (5639-4364) is 1275 bytes less (23% smaller). Let alone having to encode binary data using base64. It is the same for JSON, however there is [BSON](http://bsonspec.org)\n\nSteps to convert dhx XML to JSON:\n\n1. use [XML to JSON](http://www.utilities-online.info/xmltojson/) to convert data\n2. replace `\"-width\"` with `\"width\"`, same for `id`, `type`, `align`, `sort` -\u003e by replacing `\"-` for `\"`\n3. replace `\"#text\"` with `\"value\"`\n4. repoace `\"cell\"` with `\"data\"`\n4. replace `\"#cdata-section\"` with `\"value\"`\n5. remove `rows` level on top, remove `colums` level in head, rename `row` to `rows` below the header\n\nNow we need to update the function calls to 'eat' JSON instead of XML. The weird thing is that every dhx object seems to need a different structure.\n\n## Old Code:\n\n```javascript\n// ... CONTACTS\ncontactsGrid.load(A.server + \"contacts.xml?type=\" + A.deviceType, function () {\n  contactsGrid.selectRow(0, true);\n});\n// ... PROJECTS\nprojectsGrid.load(A.server + \"projects.xml?type=\" + A.deviceType, function () {\n  projectsGrid.selectRow(0, true);\n});\n// ... EVENTS\neventsDataView.load(A.server + \"events.xml?type=\" + A.deviceType);\n\n// ... SETTINGS\nsettingsDataView.load(A.server + \"settings.xml?type=\" + A.deviceType, function () {\n  settingsDataView.select(\"contacts\");\n});\n```\n\n## New Code:\n```javascript\n// ... CONTACTS\ncontactsGrid.load(A.server + \"contacts.json?type=\" + A.deviceType, function () {\n  contactsGrid.selectRow(0, true);\n}, \"json\");\n\n// ... PROJECTS\nprojectsGrid.load(A.server + \"projects.json?type=\" + A.deviceType, function () {\n  projectsGrid.selectRow(0, true);\n}, \"json\");\n\n// ... EVENS\neventsDataView.load(A.server + \"events.json?type=\" + A.deviceType, \"json\");\n\n// ... SETTINGS\n// load the data, somehow a callback doesn't work\nsettingsDataView.load(A.server + \"settings.json\", \"json\");\n// fires when the data loading is finished and a component or data is rendered\nsettingsDataView.attachEvent(\"onXLE\", function () {\n  settingsDataView.select(\"contacts\");\n});\n```\n\n# Step 4: Create and connect REST API\n\n[back to top](#plan)\n\n[@see branch Step4](https://github.com/rkristelijn/dhtmlx-json-node/tree/Step4)\n\n## Step 4a: set up GET for contacts with a static file\n\nThe first thing we need to do is create a router set for every element.\n\n`index.js`\n```javascript\nconst express = require('express');\nconst app = express();\nconst path = require('path');\n\nconst port = 3000;\n\napp.use('/', express.static(path.join(__dirname, 'public')));\n\n/// /* step 4\nconst apiRouter = require('./api/api-router')();\napp.use('/api', apiRouter);\n/// step 4 */\n\napp.listen(port, () =\u003e {\n  console.log('listening on *:' + port);\n});\n```\n\nThen we create a new file and put some [HATEOAS](https://spring.io/understanding/HATEOAS) in.\n\n`api\\api-router.js`\n```javascript\nconst express = require('express');\n\nlet routes = () =\u003e {\n  let apiRouter = express.Router();\n\n  apiRouter.get('/', (req, res) =\u003e {\n    res.json({\n      contacts: { links: `${req.protocol}://${req.headers.host}/api/contacts` }\n    });\n  });\n\n  return apiRouter;\n}\n\nmodule.exports = routes;\n```\n\nNow we have this:\n\n![Screenshot of json HATEOAS](/tutorial_images/Screenshot_20180530_105324.png)\n\nI use [JSON viewer](https://github.com/tulios/json-viewer) to beautify.\n\nThen we add an API for the contacts. Therefore we need to \n- move `/public/server/contacts.json` to `/api/contacts/contacts.json`\n\nAdd `api/contact/contacts-router.js`\n\n```javascript\nconst express = require('express');\n\nlet routes = () =\u003e {\n  let contactsRouter = express.Router();\n\n  contactsRouter.get('/', (req, res) =\u003e {\n    res.sendFile(__dirname + '/contacts.json');\n  });\n\n  return contactsRouter;\n}\n\nmodule.exports = routes;\n```\nAnd update `/api/api-router.js`\n\n```javascript\nconst express = require('express');\nconst contactsRouter = require('./contacts/contacts-router')(); // add this line\n\nlet routes = () =\u003e {\n  let apiRouter = express.Router();\n\n  apiRouter.use('/contacts', contactsRouter); // add this line\n\n  apiRouter.get('/', (req, res) =\u003e {\n    res.json({\n      contacts: { links: `${req.protocol}://${req.headers.host}/api/contacts` }\n    });\n  });\n\n  return apiRouter;\n}\n\nmodule.exports = routes;\n```\nFinally we need tell the front-end that the entrypoint for getting contacts is moved.\n\n`public/app.js`\n\n```javascript\ncontactsGrid.load(\"api/contacts?type=\" + A.deviceType, function () {\n  contactsGrid.selectRow(0, true);\n}, \"json\");\n```\nThe result:\n\n![Screenshot of json HATEOAS](/tutorial_images/Screenshot_20180530_112121.png)\n\n## Step 4b: set up GET for projects with a static file\n\n[back to top](#plan)\n\nSame as for contacts;\n\n- we create a folder in `api` called `projects`, \n- move the file `public/server/projects.json` to `api/projects`\n- create a new file `api/projects/projects-router.js`\n- add the route to `api/api-router.js`\n- point the load event to `/api/projects`\n\n`api/projects/projects-router.js`\n\n```javascript\nconst express = require('express');\n\nlet routes = () =\u003e {\n  let projectsRouter = express.Router();\n\n  projectsRouter.get('/', (req, res) =\u003e {\n    res.sendFile(__dirname + '/projects.json');\n  });\n\n  return projectsRouter;\n}\n\nmodule.exports = routes;\n```\n\n`api/api-router.js`\n\n```javascript\nconst express = require('express');\nconst contactsRouter = require('./contacts/contacts-router')();\nconst projectsRouter = require('./projects/projects-router')(); // add this line\n\nlet routes = () =\u003e {\n  let apiRouter = express.Router();\n\n  apiRouter.use('/contacts', contactsRouter);\n  apiRouter.use('/projects', projectsRouter); // add this line\n\n  apiRouter.get('/', (req, res) =\u003e {\n    res.json({\n      contacts: { links: `${req.protocol}://${req.headers.host}/api/contacts` },\n      projects: { links: `${req.protocol}://${req.headers.host}/api/projects` }  // add this line\n    });\n  });\n\n  return apiRouter;\n}\n\nmodule.exports = routes;\n```\n\n`public/app.js`\n\n```javascript\nprojectsGrid.load(\"api/projects?type=\" + A.deviceType, function () {\n  projectsGrid.selectRow(0, true);\n}, \"json\");\n```\n## Step 4c: events and settings\n\n[back to top](#plan)\n\nRepeat the process of projects [step4b](#step-4b-set-up-get-for-projects-with-a-static-file) for events and contacts.\n\n## Step 4d: GET using data from db\n\nFor setting up a connection to the database, we need [`mongoose`](http://mongoosejs.com/).\n\n`npm i --save mongoose`\n\nUpdate `index.js`, add:\n\n```javascript\n//...\nconst mongoose = require('mongoose');\n//...\nmongoose.connect('mongodb://localhost/cms');\nmongoose.set('debug', true);\nlet db = mongoose.connection;\n\ndb.on('error', console.error.bind(console, 'Mongoose:'));\ndb.once('open', () =\u003e {\n  console.log('Connected to mongoose');\n});\n//...\n```\n\nIf you now start your app:\n\n```bash\n[nodemon] starting `node ./index.js`\nlistening on *:3000\nConnected to mongoose\n```\n\n(Hands in the air and say: Huray!)\n\nNow we are going to create some data. We do this only once and we (mis)use the spec file for this. Later on I may want to write test. Yeah I know, test-drive development... I bother about test later, and maybe I'll start a new tutor where I first write my tests.\n\nSo first, let there be a Schema:\n\n`api/contacts/contacts-model.js`\n```javascript\nconst mongoose = require('mongoose');\n\nconst ContactsSchema = mongoose.Schema({\n  created: {\n    type: Date,\n    default: Date.now\n  },\n  photo: String,\n  name: String,\n  dob: Date,\n  pos: String,\n  email: String,\n  phone: String,\n  company: String,\n  info: String\n});\n\nconst Contact = mongoose.model('Contact', ContactsSchema);\n\nmodule.exports = Contact;\n```\n\nThen create a small file to just insert one contact:\n\n`api/contacts/contacts-model.spec.js`\n\n```javascript\nconst mongoose = require('mongoose');\nconst Contact = require('./contacts-model');\n\nmongoose.connect('mongodb://localhost/cms');\nmongoose.set('debug', true);\nlet db = mongoose.connection;\n\ndb.on('error', console.error.bind(console, 'Mongoose:'));\ndb.once('open', () =\u003e {\n  console.log('Connected to mongoose');\n});\n\ncontact = new Contact({\n  photo: \"\u003cimg src=\\\"imgs/contacts/small/margaret-black.jpg\\\" border=\\\"0\\\" class=\\\"contact_photo\\\"\u003e\",\n  name: \"Margaret Black\",\n  dob: \"9/1/1985\",\n  pos: \"CEO\",\n  email: \"mblack_ceo@mail.com\",\n  phone: \"1-805-287-4750\",\n  info: \"M Black Ltd\"\n});\n\ncontact.save(err =\u003e {\n  if (err) {\n    console.log('error', err);\n  }\n});\n```\n\nAnd now let's try it;\n\n```bash\npi@raspberry:~/dhtmlx-json-node/api/contacts $ node contacts-model.spec.js \nConnected to mongoose\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b0ea66a948a28613642d50f\"), photo: '\u003cimg src=\"imgs/contacts/small/margaret-black.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Margaret Black', dob: new Date(\"Sat, 31 Aug 1985 22:00:00 GMT\"), pos: 'CEO', email: 'mblack_ceo@mail.com', phone: '1-805-287-4750', info: 'M Black Ltd', created: new Date(\"Wed, 30 May 2018 13:26:02 GMT\"), __v: 0 })\n```\n\nThe only thing we need to do now is read the contacts.json file, loop though the data and create all contacts.\n\n`contacts-model.spec.js`\n\n```javascript\nconst mongoose = require('mongoose');\nconst Contact = require('./contacts-model');\nconst fs = require('fs');\n\nmongoose.connect('mongodb://localhost/cms');\nmongoose.set('debug', true);\nlet db = mongoose.connection;\n\ndb.on('error', console.error.bind(console, 'Mongoose:'));\ndb.once('open', () =\u003e {\n  console.log('Connected to mongoose');\n});\n\nfs.readFile('./contacts.json', (err, data) =\u003e {\n  if (err) console.log('error', err);\n  obj = JSON.parse(data);\n  for (contact of obj.rows) {\n    console.log(`Creating ${contact.data[1]}...`);\n    contact = new Contact({\n      photo: contact.data[0],\n      name: contact.data[1],\n      dob: contact.data[2],\n      pos: contact.data[3],\n      email: contact.data[4],\n      phone: contact.data[5],\n      company: contact.data[6],\n      info: contact.data[7]\n    });\n\n    contact.save(err =\u003e {\n      if (err) {\n        console.log('error', err);\n      }\n    });\n  }\n});\n\n\n```\nLet's run it: `node contacts-model.spec.js` but first, empty the table to make sure all data is clean\n\n```bash\npi@raspberry:~/dhtmlx-json-node/api/contacts $ mongo\nMongoDB shell version: 3.2.11\nconnecting to: test\nServer has startup warnings: \n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] \n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] ** WARNING: This 32-bit MongoDB binary is deprecated\n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] \n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] \n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] ** NOTE: This is a 32 bit MongoDB binary.\n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] **       32 bit builds are limited to less than 2GB of data (or less with --journal).\n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] **       See http://dochub.mongodb.org/core/32bit\n2018-06-04T11:12:23.300+0200 I CONTROL  [initandlisten] \n\u003e use cms\nswitched to db cms\n\u003e db.contacts.remove({})\nWriteResult({ \"nRemoved\" : 17 })\n\u003e exit\nbye\npi@raspberry:~/dhtmlx-json-node/api/contacts $ node contacts-model.spec.js \nCreating Margaret Black...\nCreating John Woken...\nCreating Jake Peterson...\nCreating Bill Jackson...\nCreating Jennifer Miles...\nCreating Cortny Barrens...\nCreating Edward Eden...\nCreating Andrew Scott...\nCreating Steve Anderson...\nCreating Jane Wilson...\nCreating Alan Robbinson...\nCreating William Parson...\nCreating Charlotte Wolks...\nCreating Pamela Worner...\nCreating Ralf Ross...\nCreating Dan Witley...\nCreating Anna Harrison...\nConnected to mongoose\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf1\"), photo: '\u003cimg src=\"imgs/contacts/small/margaret-black.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Margaret Black', dob: '9/1/1985', pos: 'CEO', email: 'mblack_ceo@mail.com', phone: '1-805-287-4750', company: 'M Black Ltd', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf2\"), photo: '\u003cimg src=\"imgs/contacts/small/john-woken.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'John Woken', dob: '3/24/1987', pos: 'Business analyst', email: 'john.woken@mail.com', phone: '1-867-777-9834', company: 'M-Black Ltd', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf3\"), photo: '\u003cimg src=\"imgs/contacts/small/jake-peterson.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Jake Peterson', dob: '11/27/1982', pos: 'Accountant', email: 'jake.peterson@mail.com', phone: '1-845-257-9751', company: 'Jackson and partners Inc', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf4\"), photo: '\u003cimg src=\"imgs/contacts/small/bill-jackson.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Bill Jackson', dob: '5/3/1980', pos: 'Web developer', email: 'Bill.Jackson@mail.com', phone: '1-874-548-9751', company: 'BFG Consulting Inc', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf5\"), photo: '\u003cimg src=\"imgs/contacts/small/jennifer-miles.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Jennifer Miles', dob: '4/17/1985', pos: 'Project manager', email: 'Jennifer.Miles@mail.com', phone: '1-852-895-9752', company: 'F\u0026M Ltd', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf6\"), photo: '\u003cimg src=\"imgs/contacts/small/cortny-barrens.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Cortny Barrens', dob: '6/20/1979', pos: 'Sales manager', email: 'Cortny.Barrens@mail.com', phone: '1-842-458-1452', company: 'F\u0026M Ltd', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf7\"), photo: '\u003cimg src=\"imgs/contacts/small/edward-eden.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Edward Eden', dob: '1/14/1983', pos: 'Business analyst', email: 'Edward.Eden@mail.com', phone: '1-863-452-4750', company: 'BFG Consulting Inc', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf8\"), photo: '\u003cimg src=\"imgs/contacts/small/andrew-scott.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Andrew Scott', dob: '2/15/1980', pos: 'HR manager', email: 'Andrew.Scott@mail.com', phone: '1-874-452-4873', company: 'Jackson and partners Inc', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bf9\"), photo: '\u003cimg src=\"imgs/contacts/small/steve-anderson.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Steve Anderson', dob: '4/17/1978', pos: 'Business analyst', email: 'Steve.Anderson@mail.com', phone: '1-863-548-4874', company: 'Bank of LA', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bfa\"), photo: '\u003cimg src=\"imgs/contacts/small/jane-wilson.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Jane Wilson', dob: '6/25/1980', pos: 'Product manager', email: 'Jane.Wilson@mail.com', phone: '1-863-452-9834', company: 'HDF Insurance', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bfb\"), photo: '\u003cimg src=\"imgs/contacts/small/alan-robbinson.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Alan Robbinson', dob: '8/16/1970', pos: 'Web developer', email: 'Alan.Robbinson@mail.com', phone: '1-863-452-9752', company: 'Meriton Group', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bfc\"), photo: '\u003cimg src=\"imgs/contacts/small/william-parson.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'William Parson', dob: '10/2/1969', pos: 'Cheif engineer', email: 'William.Parson@mail.com', phone: '1-874-452-4877', company: 'ANG Learning', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bfd\"), photo: '\u003cimg src=\"imgs/contacts/small/charlotte-wolks.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Charlotte Wolks', dob: '12/4/1989', pos: 'Marketing specialist', email: 'Charlotte.Wolks@mail.com', phone: '1-863-452-4750', company: 'HDF Insurance', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bfe\"), photo: '\u003cimg src=\"imgs/contacts/small/pamela-worner.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Pamela Worner', dob: '11/17/1976', pos: 'Business analyst', email: 'Pamela.Worner@mail.com', phone: '1-863-548-4879', company: 'ANG Learning', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13bff\"), photo: '\u003cimg src=\"imgs/contacts/small/ralf-ross.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Ralf Ross', dob: '9/29/1973', pos: 'Sales manager', email: 'Ralf.Ross@mail.com', phone: '1-863-452-9751', company: 'Bank of LA', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13c00\"), photo: '\u003cimg src=\"imgs/contacts/small/dan-witley.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Dan Witley', dob: '1/5/1978', pos: 'Web developer', email: 'Dan.Witley@mail.com', phone: '1-874-452-9834', company: 'Bank of LA', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b150f6369957c198ff13c01\"), photo: '\u003cimg src=\"imgs/contacts/small/anna-harrison.jpg\" border=\"0\" class=\"contact_photo\"\u003e', name: 'Anna Harrison', dob: '5/8/1984', pos: 'QA engineer', email: 'Anna.Harrison@mail.com', phone: '1-863-548-9751', company: 'Meriton Group', created: new Date(\"Mon, 04 Jun 2018 10:07:31 GMT\"), __v: 0 })\n^C\npi@raspberry:~/dhtmlx-json-node/api/contacts $ ^C\npi@raspberry:~/dhtmlx-json-node/api/contacts $ mongo\nMongoDB shell version: 3.2.11\nconnecting to: test\nServer has startup warnings: \n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] \n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] ** WARNING: This 32-bit MongoDB binary is deprecated\n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] \n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] \n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] ** NOTE: This is a 32 bit MongoDB binary.\n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] **       32 bit builds are limited to less than 2GB of data (or less with --journal).\n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] **       See http://dochub.mongodb.org/core/32bit\n2018-05-30T14:25:48.215+0200 I CONTROL  [initandlisten] \n\u003e use cms\nswitched to db cms\n\u003e show collections\ncontacts\nsystem.indexes\n\u003e db.contacts.find()\n{ \"_id\" : ObjectId(\"5b150f6369957c198ff13bf6\"), \"photo\" : \"\u003cimg src=\\\"imgs/contacts/small/cortny-barrens.jpg\\\" border=\\\"0\\\" class=\\\"contact_photo\\\"\u003e\", \"name\" : \"Cortny Barrens\", \"dob\" : \"6/20/1979\", \"pos\" : \"Sales manager\", \"email\" : \"Cortny.Barrens@mail.com\", \"phone\" : \"1-842-458-1452\", \"company\" : \"F\u0026M Ltd\", \"created\" : ISODate(\"2018-06-04T10:07:31.128Z\"), \"__v\" : 0 }\n ... \nType \"it\" for more\n\u003e exit\nbye\n\n```\n\nNow that the data is in, let's try and get it via the script.\n\nFor this we need a controller:\n\n`api/contacts/contacts-controller.js`\n\n```javascript\nlet contactsController = (Model) =\u003e {\n  // hardcoded header\n  let _head = [\n    { \"id\": \"photo\", \"width\": \"65\", \"type\": \"ro\", \"align\": \"center\", \"sort\": \"na\", \"value\": \"\u003cspan style='padding-left:60px;'\u003eName\u003c/span\u003e\" },\n    { \"id\": \"name\", \"width\": \"150\", \"type\": \"ro\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"#cspan\" },\n    { \"id\": \"dob\", \"width\": \"130\", \"type\": \"ro\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Date of Birth\" },\n    { \"id\": \"pos\", \"width\": \"130\", \"type\": \"ro\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Position\" },\n    { \"id\": \"email\", \"width\": \"170\", \"type\": \"ro\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"E-mail Address\" },\n    { \"id\": \"phone\", \"width\": \"150\", \"type\": \"ro\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Phone\" },\n    { \"id\": \"company\", \"width\": \"150\", \"type\": \"ro\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Company\" },\n    { \"id\": \"info\", \"width\": \"*\", \"type\": \"ro\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Additional\" }];\n\n  // just find all data in table\n  let _readAll = (callback) =\u003e {\n    Model.find({}, (err, contacts) =\u003e {\n      if (err) callback(err, null);\n      else callback(null, { head: _head, rows: _toRows(contacts) });\n    });\n  };\n\n  // create {id:x,data:[y,z,...]} from {_id:x,y:'',z:''}\n  let _toRows = (rows) =\u003e {\n    let result = [];\n    for (row of rows) {\n      result.push({\n        id: row._id, data: [row.photo, row.name, row.dob, row.pos, row.email, row.phone, row.company, row.info]\n      });\n    }\n    return result;\n  };\n\n  // revealing model pattern, not revealing _toRows()\n  return {\n    readAll: _readAll\n  };\n}\n\nmodule.exports = contactsController;\n```\n\nAnd we need to call the controller when the route is used:\n\n`api/contacts/contacts-router.js`\n\n```javascript\nconst express = require('express');\nconst Contacts = require('./contacts-model');\nconst contactsController = require('./contacts-controller')(Contacts);\n\nlet routes = () =\u003e {\n  let contactsRouter = express.Router();\n\n  contactsRouter.get('/', (req, res) =\u003e {\n    contactsController.readAll((err, contacts) =\u003e {\n      if (err) {\n        res.sendStatus(400).send(err);\n      } else {\n        res.json(contacts);\n      }\n    });\n  });\n\n  return contactsRouter;\n}\n\nmodule.exports = routes;\n```\nNow the data is retrieved from the database.\n\nLet's review what is done now with the following viewpoints; \n\n### [Separation of Concerns (SoC)](https://en.wikipedia.org/wiki/Separation_of_concerns)\n\nThe router only is [concerned](https://en.wikipedia.org/wiki/Separation_of_concerns) with HTTP traffic. No DB stuff in here, only proper error handling. Yes, there is a dependency on the `Contacts` model however this is where the following comes in.\n\n### [Polymorphism](https://en.wikipedia.org/wiki/Polymorphism_(computer_science))\n\nBecause of the controller uses [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) the controller can be hypothetically be used for other controller, if the fields are passed with it. This is maybe something we do in future. So here's a list of task that can be done to improve the code even more:\n\n- [ ] pass the structure of the model along with the constructor\n- [ ] improve the data population script so it ends after running it, but only after all db actions are completed.\n- [ ] improve the error handler so that dhx also knows what is going on if an error occurs\n- [ ] make the Date of Birth a real date\n\n## Step 4e: PUT for updates\n\n[@see branch Step4e](https://github.com/rkristelijn/dhtmlx-json-node/tree/Step4e)\n\n[back to top](#plan)\n\nNow that the GET is working, let's implement the PUT (update).\n\nPlan of approach\n\n- [ ] Implement PUT on the API\n- [ ] Make the grid editable\n- [ ] Implement fetch\n- [ ] Implement onBlur of form\n\n### PUT on API\n\nAs we want to know what is going in in our HTTP traffic, we use `express-log`.\n\n`npm i --save-dev express-log`\n\nBecause we need to parse the body that is being send, we need `body-parser`\n\n`npm i --save body-parser`\n\nImplement both:\n\n`index.js`\n\n```javascript\n// add the following lines to the top\nconst bodyParser = require('body-parser');\nconst logger = require('express-log');\n\n//...\n\n// set up middleware\napp.use(logger());\napp.use(bodyParser.json());\napp.use(bodyParser.urlencoded({ extended: true }));\n\n```\n\nNow we can add the update step in the controller:\n\n`api/contacts/contacts-controller.js`\n\n```javascript\n// find one and update with one atomic operation, forcing the altered document to return\nlet _updateOne = (id, data, callback) =\u003e {\n  Model.findOneAndUpdate({ _id: id }, data, { new: true }, (err, contact) =\u003e {\n    if (err) callback(err, null);\n    else callback(null, contact);\n  });\n};\n```\n\nAnd connect the router to the controller:\n\n`api/contacts/contacts-router.js`\n\n```javascript\ncontactsRouter.put('/:id', (req, res) =\u003e {\n  contactsController.updateOne(req.params.id, req.body, (err, contact) =\u003e {\n    if (err) {\n      res.sendStatus(400).end(err);\n    } else {\n      res.json(contact);\n    }\n  });\n});\n```\n\nLet's use postman to send a PUT request. Keep in mind the following;\n\n- [ ] Your id is different, look in the GET call to get your own id\n- [ ] It is important to set `Content-Type` to `application/json` in the header\n\n```json\n{\n\t\"photo\": \"\u003cimg src='imgs/contacts/small/cortny-barrens.jpg' border='0' class='contact_photo'\u003e\",\n\t\"name\": \"Cortny Barrens 2\",\n\t\"dob\": \"6/20/1979\",\n\t\"pos\": \"Sales manager\",\n\t\"email\": \"Cortny.Barrens@mail.com\",\n\t\"phone\":\"1-842-458-1452\",\n\t\"company\":\"F\u0026M Ltd\"\n}\n```\n\n![Postman showing a PUT request and a returned object](/tutorial_images/Screenshot_20180605_090250.png)\n\nThe only thing left is to connect the front-end. Instead of using [`dhtmlxDataProcessor`](https://docs.dhtmlx.com/dataprocessor__index.html) on browser-side, connected to the [`dhtmlxConnector`](https://dhtmlx.com/docs/products/dhtmlxConnector/) server-side, I'm going to wire up the grid directly to fetch.\n\n![dhtmlxDataProcessor and dhtmlxConnector interaction diagram](tutorial_images/dataprocessor_front.png).\n\nTo create this we need basic understanding of the events of the grid.\n\nI've added all available event-handlers that are mentioned in the documentation. Because of the various number of arguments, some need to return something and the lack of mention of the event-name, I needed to add all the events separately.\n\nCreate a new file\n\n`public/dhx-fetch-dp-js`\n\n```javascript\nfunction attachDp(obj) {\n  //fires after a row has been deleted from the grid\n  obj.attachEvent('onAfterRowDeleted', (id, pid) =\u003e {\n    console.log('onAfterRowDeleted', id, pid);\n  });\n  //fires when the user starts selecting a block\n  obj.attachEvent('onBeforeBlockSelected', (rId, cInd) =\u003e {\n    console.log('onBeforeBlockSelected', rId, cInd);\n  });\n  //fires when the drag operation starts\n  obj.attachEvent('onBeforeDrag', (id) =\u003e {\n    console.log('onBeforeDrag', id);\n  });\n  //fires right before a row is deleted\n  obj.attachEvent('onBeforeRowDeleted', (rId) =\u003e {\n    console.log('onBeforeRowDeleted', rId);\n  });\n  //fires after clicking by the right mouse button on the selection block\n  obj.attachEvent('onBlockRightClick', (block, object) =\u003e {\n    console.log('onBlockRightClick', block, object);\n  });\n  //fires when a calendar pops up in the grid\n  obj.attachEvent('onCalendarShow', (myCal, rowId, colInd) =\u003e {\n    console.log('onCalendarShow', myCal, rowId, colInd);\n  });\n  //fires immediately after a cell has been selected\n  obj.attachEvent('onCellMarked', (rId, ind) =\u003e {\n    console.log('onCellMarked', rId, ind);\n  });\n  //fires immediately after a cell is unselected\n  obj.attachEvent('onCellUnMarked', (rId, ind) =\u003e {\n    console.log('onCellUnMarked', rId, ind);\n  });\n  //fires after the state of a checkbox has been changed\n  obj.attachEvent('onCheck', (rId, cInd, state) =\u003e {\n    console.log('onCheck', rId, cInd, state);\n  });\n  //the event is deprecated, use the onCheck event instead; fires after the state was changed\n  // obj.attachEvent('onCheckbox', (rId, cInd, state) =\u003e {\n  //   console.log('onCheckbox', rId, cInd, state);\n  // });\n  //fires when the grid is cleared (reloaded)\n  obj.attachEvent('onClearAll', () =\u003e {\n    console.log('onClearAll');\n  });\n  //fires after the values have been collected to fill the select filter\n  obj.attachEvent('onCollectValues', (index) =\u003e {\n    console.log('onCollectValues', index);\n    return true; //mandatory for the default processing\n  });\n  //fires when the data is loaded to the grid but hasn't been rendered yet\n  obj.attachEvent('onDataReady', () =\u003e {\n    console.log('onDataReady');\n  });\n  //fires on calendar's initialization on the page\n  obj.attachEvent('onDhxCalendarCreated', (myCal) =\u003e {\n    console.log('onDhxCalendarCreated', myCal);\n  });\n  //fires on the end of distributed parsing\n  obj.attachEvent('onDistributedEnd', () =\u003e {\n    console.log('onDistributedEnd');\n  });\n  //fires when an item is dragged to another target and the mouse is released, the event can be blocked\n  obj.attachEvent('onDrag', (sId, tId, sObj, tObj, sInd, tInd) =\u003e {\n    console.log('onDrag', sId, tId, sObj, tObj, sInd, tInd);\n  });\n  //fires before requesting additional data from the server in case of dynamic Smart Rendering or dynamic Paging\n  obj.attachEvent('onDynXLS', (start, count) =\u003e {\n    console.log('onDynXLS', start, count);\n  });\n  //fires when the edit operation was canceled\n  obj.attachEvent('onEditCancel', (rowId, colInd, value) =\u003e {\n    console.log('onEditCancel', rowId, colInd, value);\n  });\n  //fires 1-3 times depending on cell's editability (see the stage parameter)\n  obj.attachEvent('onEditCell', (stage, rId, cInd, nValue, oValue) =\u003e {\n    console.log('onEditCell', stage, rId, cInd, nValue, oValue);\n  });\n  //fires on clicking the dhtmlxgrid area which is not filled with data\n  obj.attachEvent('onEmptyClick', (ev) =\u003e {\n    console.log('onEmptyClick', ev);\n  });\n  //fires immediately after the Enter key was pressed\n  obj.attachEvent('onEnter', (id, ind) =\u003e {\n    console.log('onEnter', id, ind);\n  });\n  //fires when filtering is completed (filtering extension)\n  obj.attachEvent('onFilterEnd', (elements) =\u003e {\n    console.log('onFilterEnd', elements);\n  });\n  //fires when filtering has been activated but before the real filtering started\n  obj.attachEvent('onFilterStart', (indexes, values) =\u003e {\n    console.log('onFilterStart', indexes, values);\n    return true; //The event is blockable. Returning false will block the default action. returning true will confirm filtering\n  });\n  //fires immediately after a row has been added/deleted or the grid has been reordered\n  obj.attachEvent('onGridReconstructed', (obj) =\u003e {\n    console.log('onGridReconstructed', obj);\n  });\n  //fires when a grid was grouped by some column\n  obj.attachEvent('onGroup', () =\u003e {\n    console.log('onGroup');\n  });\n  //fires when a group was opened/closed\n  obj.attachEvent('onGroupStateChanged', (value, state) =\u003e {\n    console.log('onGroupStateChanged', value, state);\n  });\n  //fires on pressing the Down-Arrow button while the last row of the page is selected\n  obj.attachEvent('onLastRow', () =\u003e {\n    console.log('onLastRow');\n  });\n  //fires when validation runs successfully\n  obj.attachEvent('onLiveValidationCorrect', (id, index, value, input, rule) =\u003e {\n    console.log('onLiveValidationCorrect', id, index, value, input, rule);\n  });\n  //fires when validation runs and rules execution are failed\n  obj.attachEvent('onLiveValidationError', (id, index, value, input, rule) =\u003e {\n    console.log('onLiveValidationError', id, index, value, input, rule);\n  });\n  //fires when the mouse pointer is moved over a cell\n  obj.attachEvent('onMouseOver', (id, ind) =\u003e {\n    console.log('onMouseOver', id, ind);\n  });\n  //fires on each resize iteration\n  obj.attachEvent('onResize', (cInd, cWidth, obj) =\u003e {\n    console.log('onResize', cInd, cWidth, obj);\n  });\n  //fires when resizing of a column is finished\n  obj.attachEvent('onResizeEnd', (obj) =\u003e {\n    console.log('onResizeEnd', obj);\n  });\n  //fires immediately after the right mouse button has been clicked on a grid's row\n  obj.attachEvent('onRightClick', (id, ind, obj) =\u003e {\n    console.log('onRightClick', id, ind, obj);\n  });\n  //fires right after a row has been added to the grid\n  obj.attachEvent('onRowAdded', (rId) =\u003e {\n    console.log('onRowAdded', rId);\n  });\n  //fires when the row is hiding\n  obj.attachEvent('onRowHide', (id, state) =\u003e {\n    console.log('onRowHide', id, state);\n  });\n  //fires after the ID of a row has been changed (changeRowId, setRowId, dataprocessor)\n  obj.attachEvent('onRowIdChange', (oldId, newId) =\u003e {\n    console.log('onRowIdChange', oldId, newId);\n  });\n  //fires when the row is added to the grid and filled with data\n  obj.attachEvent('onRowInserted', (row, rInd) =\u003e {\n    console.log('onRowInserted', row, rInd);\n  });\n  //fires for each row pasted from the clipboard (block selection extension)\n  obj.attachEvent('onRowPaste', (rId) =\u003e {\n    console.log('onRowPaste', rId);\n  });\n  //fires immediately after a row in the grid has been clicked\n  obj.attachEvent('onRowSelect', (id, ind) =\u003e {\n    console.log('onRowSelect', id, ind);\n  });\n  //fires immediately after scrolling has occured\n  obj.attachEvent('onScroll', (sLeft, sTop) =\u003e {\n    console.log('onScroll', sLeft, sTop);\n  });\n  //fires immediately when the selection state has been changed\n  obj.attachEvent('onSelectStateChanged', (id) =\u003e {\n    console.log('onSelectStateChanged', id);\n  });\n  //fires after the stat values have been calculated\n  obj.attachEvent('onStatReady', () =\u003e {\n    console.log('onStatReady');\n  });\n  //fires when sub-row-ajax cell loads its data\n  obj.attachEvent('onSubAjaxLoad', (id, content) =\u003e {\n    console.log('onSubAjaxLoad', id, content);\n  });\n  //fires when the creation of a sub-grid was initialized (can be blocked)\n  obj.attachEvent('onSubGridCreated', (obj, rowId, rowIndex) =\u003e {\n    console.log('onSubGridCreated', obj, rowId, rowIndex);\n    return true;\n  });\n  //fires when a sub-row(sub-grid) was opened/closed\n  obj.attachEvent('onSubRowOpen', (id, state) =\u003e {\n    console.log('onSubRowOpen', id, state);\n  });\n  //fires when data synchronization is finished\n  obj.attachEvent('onSyncApply', () =\u003e {\n    console.log('onSyncApply');\n  });\n  //fires during the tabulation in the grid, blockable\n  obj.attachEvent('onTab', (mode) =\u003e {\n    console.log('onTab', mode);\n  });\n  //fires when the grid was ungrouped\n  obj.attachEvent('onUnGroup', () =\u003e {\n    console.log('onUnGroup');\n  });\n  //fires when validation runs successfully\n  obj.attachEvent('onValidationCorrect', (id,index,value,rule) =\u003e {\n    console.log('onValidationCorrect', id,index,value,rule);\n  });\n  //fires when validation runs and rules execution are failed\n  obj.attachEvent('onValidationError', (id,index,value,rule) =\u003e {\n    console.log('onValidationError', id,index,value,rule);\n  });\n}\n```\nThen include the file for loading\n\n`public/index.html`\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"//cdn.dhtmlx.com/edge/skins/web/dhtmlx.css\"\u003e\n  \u003cscript src=\"//cdn.dhtmlx.com/edge/dhtmlx.js\"\u003e\u003c/script\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\" src=\"dhx-fetch-dp.js\"\u003e\u003c/script\u003e\u003c!--include before--\u003e\n  \u003cscript type=\"text/javascript\" src=\"app.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n\nAnd call the event bindings\n\n`public/app.js`\n\n```javascript\n// ...\ncontactsGrid = contactsLayout.cells(\"a\").attachGrid();\ncontactsGrid.load(\"api/contacts?type=\" + A.deviceType, function () {\n  contactsGrid.selectRow(0, true);\n}, \"json\");\n\nattachDp(contactsGrid);\n// attach form\ncontactsGrid.attachEvent(\"onRowSelect\", contactsFillForm);\ncontactsGrid.attachEvent(\"onRowInserted\", contactsGridBold);\n// ...\n```\n\n![Screenshot of the events in console](tutorial_images/Screenshot_20180606_141400.png)\n\nNow you can click on rows, press tab/enter/shift-tab/arrow-keys, move headings, rightclick and you see the events fire in the console window.\n\nLooking at the events, the `onEditCell` looks like the most suitable for the PUT request.\n\n```javascript\n//fires 1-3 times depending on cell's editability (see the stage parameter)\nobj.attachEvent('onEditCell', (stage, rowId, colIndex, newValue, oldValue) =\u003e {\n  //stage the stage of editing (0-before start; can be canceled if return false,1 - the editor is opened,2- the editor is closed)\n  const beforeStart = 0;\n  const editorOpened = 1;\n  const editorClosed = 2;\n\n  if (stage === editorClosed \u0026 newValue !== oldValue) {\n    let fieldName = obj.getColumnId(colIndex);\n    let request = `{\"${fieldName}\":\"${newValue}\"}`;\n    console.log('request',  JSON.parse(request));\n    fetch(`/api/contacts/${rowId}`, {\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      method: 'PUT',\n      body: request\n    })\n      .then(response =\u003e response.json())\n      .then(response =\u003e {\n        console.log('response', response);\n      })\n      .catch(err =\u003e { console.error(err) });\n\n    return true;\n  }\n});\n```\n\nAlso you need to change `api/contacts/contacts-controller.js` to allow editing `ro` to `ed`\n\n```javascript\n  let _head = [\n    { \"id\": \"photo\", \"width\": \"65\", \"type\": \"ro\", \"align\": \"center\", \"sort\": \"na\", \"value\": \"\u003cspan style='padding-left:60px;'\u003eName\u003c/span\u003e\" },\n    { \"id\": \"name\", \"width\": \"150\", \"type\": \"ed\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"#cspan\" },\n    { \"id\": \"dob\", \"width\": \"130\", \"type\": \"ed\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Date of Birth\" },\n    { \"id\": \"pos\", \"width\": \"130\", \"type\": \"ed\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Position\" },\n    { \"id\": \"email\", \"width\": \"170\", \"type\": \"ed\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"E-mail Address\" },\n    { \"id\": \"phone\", \"width\": \"150\", \"type\": \"ed\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Phone\" },\n    { \"id\": \"company\", \"width\": \"150\", \"type\": \"ed\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Company\" },\n    { \"id\": \"info\", \"width\": \"*\", \"type\": \"ed\", \"align\": \"left\", \"sort\": \"na\", \"value\": \"Additional\" }\n  ];\n```\nAnd you need to set the editEvents in `public/app.js`\n\n```javascript\n// attach grid\ncontactsGrid = contactsLayout.cells(\"a\").attachGrid();\ncontactsGrid.enableEditEvents(true,true,true);\ncontactsGrid.init();\n```\n\nNote to above:\n\n- [ ] There is no proper error handling (yet), it just assumes it works always.\n- [ ] The dataprocessor is now linked to a specific API-url, no no reuse for other grids or object (yet)\n- [ ] The dataprocessor assumes it is linked to a grid\n\nLet's have a look at the result;\n\n![Application showing the request and the response in the console when updating any field in the contactsGrid](tutorial_images/Screenshot_20180607_101650.png)\n\n### Form\n\nNext up is the form. Let's do the same and connect all the events in a different js script\n\n- rename `dhx-fetch-dp.js` to `dhx-fetch-dp-grid.js`\n- create a new file `dhx-fetch-dp-form.js`\n- include both files in `index.html`\n- add another argument (`objectName`) of the function and rename the function in `dhx-fetch-dp-grid.js`, also prefix the `console.log()` with `objectName` using search/replace, or get it from the branch\n\n`public/dhx-fetch-dp-grid.js`\n\n```javascript\nfunction attachDpGrid(obj, objectName) {\n  //fires after a row has been deleted from the grid\n  obj.attachEvent('onAfterRowDeleted', (id, pid) =\u003e {\n    console.log(objectName, 'onAfterRowDeleted', id, pid);\n  });\n// ...\n```\n\n`public/index.html`\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"//cdn.dhtmlx.com/edge/skins/web/dhtmlx.css\"\u003e\n  \u003cscript src=\"//cdn.dhtmlx.com/edge/dhtmlx.js\"\u003e\u003c/script\u003e\n  \u003clink rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"\u003e\n\u003c/head\u003e\n\n\u003cbody\u003e\n  \u003cscript type=\"text/javascript\" src=\"dhx-fetch-dp-grid.js\"\u003e\u003c/script\u003e\n  \u003cscript type=\"text/javascript\" src=\"dhx-fetch-dp-form.js\"\u003e\u003c/script\u003e\n  \u003cscript type=\"text/javascript\" src=\"app.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n\n\u003c/html\u003e\n```\n`public/dhx-fetch-dp-form.js`\n\n```javascript\nfunction attachDpForm(obj, objectName) {\n  // onAfterReset\tfires after resetting the form\n  obj.attachEvent('onAfterReset', (formId) =\u003e {\n    console.log(objectName, 'onAfterReset', formId);\n  });\n  // onAfterSave\tfires after data has been saved in DB\n  obj.attachEvent('onAfterSave', (formId, response) =\u003e {\n    console.log(objectName, 'onAfterSave', formId, response);\n  });\n  // onAfterValidate\tfires after the validation\n  obj.attachEvent('onAfterValidate', (success) =\u003e {\n    console.log(objectName, 'onAfterValidate', success);\n  });\n  // onBeforeChange\tfires before the data in some input changed ( by user actions )\n  obj.attachEvent('onBeforeChange', (itemName, oldValue, newValue) =\u003e {\n    console.log(objectName, 'onBeforeChange', itemName, oldValue, newValue);\n    return true;\n  });\n  // onBeforeClear\tfires before the user clears the list of files to upload (clicks the button )\n  obj.attachEvent('onBeforeClear', () =\u003e {\n    console.log(objectName, 'onBeforeClear');\n    return true;\n  });\n  // onBeforeDataLoad\tfires after the data for the form received, but before it's assigned to actual fields\n  obj.attachEvent('onBeforeDataLoad', (formId, values) =\u003e {\n    console.log(objectName, 'onBeforeDataLoad', formId, values);\n    return true;\n  });\n  // onBeforeFileAdd\tfires when the user adds a file to the upload queue\n  obj.attachEvent('onBeforeFileAdd', (fileName) =\u003e {\n    console.log(objectName, 'onBeforeFileAdd', fileName);\n    return true;\n  });\n  // onBeforeFileRemove\tfires before the user removes a single file from the list of files to upload (clicks the button )\n  obj.attachEvent('onBeforeFileRemove', (clientFileName, serverFileName) =\u003e {\n    console.log(objectName, 'onBeforeFileRemove', clientFileName, serverFileName);\n    return true;\n  });\n  // onBeforeFileUpload\tfires before file uploading has started\n  obj.attachEvent('onBeforeFileUpload', (mode, loader, formData) =\u003e {\n    console.log(objectName, 'onBeforeFileUpload', mode, loader, formData);\n    return true;\n  });\n  // onBeforeReset\tfires before resetting the form\n  obj.attachEvent('onBeforeReset', (formId, values) =\u003e {\n    console.log(objectName, 'onBeforeReset', formId, values);\n    return true;\n  });\n  // onBeforeSave\tfires before saving the form\n  obj.attachEvent('onBeforeSave', (formId, values) =\u003e {\n    console.log(objectName, 'onBeforeSave', formId, values);\n    return true;\n  });\n  // onBeforeValidate\tfires when validation has started but is not applied yet\n  obj.attachEvent('onBeforeValidate', (formId) =\u003e {\n    console.log(objectName, 'onBeforeValidate', formId);\n    return true;\n  });\n  // onBlur\tfires when the user moves the mouse pointer out of the input\n  obj.attachEvent('onBlur', (itemName, value) =\u003e {\n    // value: item value (for checkboxes and radios only)\n    console.log(objectName, 'onBlur', itemName, value);\n  });\n  // onButtonClick\tfires when the user clicks a button\n  obj.attachEvent('onButtonClick', (itemName) =\u003e {\n    console.log(objectName, 'onButtonClick', itemName);\n  });\n  // onChange\tfires when data in some input was changed\n  obj.attachEvent('onChange', (itemName, value, state) =\u003e {\n    //state = checked/unchecked (for checkboxes and radios only)\n    console.log(objectName, 'onChange', itemName, value, state);\n  });\n  // onClear\twhen the user clears the list of files to upload (clicks on button )\n  obj.attachEvent('onClear', () =\u003e {\n    console.log(objectName, 'onClear');\n  });\n  // onDisable\tfires when the container is disabled after being enabled\n  obj.attachEvent('onDisable', (itemName) =\u003e {\n    console.log(objectName, 'onDisable', itemName);\n  });\n  // onEditorAccess\tfires when the user accesses the editor body\n  obj.attachEvent('onEditorAccess', (name, type, event, editor, form) =\u003e {\n    // name\tstring\tthe item's name\n    // type\tstring\tthe type of the accessing action (e.g. \"mousedown\",\"focus\",\"mouseup\",\"click\", \"blur\")\n    // ev\tobject\tthe event object\n    // editor\tobject\tthe editor instance\n    // form\tobject\tthe form instance\n    console.log(objectName, 'onEditorAccess', name, type, event, editor, form);\n  });\n  // onEditorToolbarClick\tfires when the user clicks the editor toolbar\n  obj.attachEvent('onEditorToolbarClick', (name, toolbarId, editor, form) =\u003e {\n    console.log(objectName, 'onEditorToolbarClick', name, toolbarId, editor, form);\n  });\n  // onEnable\tfires when the container control is enabled after being disabled\n  obj.attachEvent('onEnable', (itemName) =\u003e {\n    console.log(objectName, 'onEnable', itemName);\n  });\n  // onEnter\tfires on pressing the Enter button when the mouse pointer is set in an input control\n  obj.attachEvent('onEnter', (itemName) =\u003e {\n    console.log(objectName, 'onEnter', itemName);\n  });\n  // onFileAdd\tfires when the user adds a file to the upload queue\n  obj.attachEvent('onFileAdd', (fileName) =\u003e {\n    console.log(objectName, 'onFileAdd', fileName);\n  });\n  // onFileRemove\tfires when the user removes single file from the list of files to upload (clicks the button )\n  obj.attachEvent('onFileRemove', (clientFileName, serverFileName) =\u003e {\n    console.log(objectName, 'onFileRemove', clientFileName, serverFileName);\n  });\n  // onFocus\tfires when an input gets focus\n  obj.attachEvent('onFocus', (itemName, value) =\u003e {\n    // value: item value (for checkboxes and radios only)\n    console.log(objectName, 'onFocus', itemName, value);\n  });\n  // onImageUploadFail\tfires when an image was uploaded incorrectly\n  obj.attachEvent('onImageUploadFail', (itemName, extra) =\u003e {\n    console.log(objectName, 'onImageUploadFail', itemName, extra);\n  });\n  // onImageUploadSuccess\tfires when an image was uploaded correctly\n  obj.attachEvent('onImageUploadSuccess', (itemName, imageName, extra) =\u003e {\n    console.log(objectName, 'onImageUploadSuccess', itemName, imageName, extra);\n  });\n  // onInfo\tfires when the user clicks the Info icon\n  obj.attachEvent('onInfo', (itemName, event) =\u003e {\n    console.log(objectName, 'onInfo', itemName, event);\n  });\n  // onInputChange\tfires when data in an input was changed and the cursor is still in this input\n  obj.attachEvent('onInputChange', (itemName, value, form) =\u003e {\n    console.log(objectName, 'onInputChange', itemName, value, form);\n  });\n  // onKeydown\tfires when the native \"onkeydown\" event is triggered\n  obj.attachEvent('onKeydown', (inputElement, eventObject, itemName, value) =\u003e {\n    console.log(objectName, 'onKeydown', inputElement, eventObject, itemName, value);\n  });\n  // onKeyup\tfires when the native \"onkeyup\" event is triggered\n  obj.attachEvent('onKeyup', (inputElement, eventObject, itemName, value) =\u003e {\n    console.log(objectName, 'onKeyup', inputElement, eventObject, itemName, value);\n  });\n  // onOptionsLoaded\tfires after the item options were completely loaded from the server to the client\n  obj.attachEvent('onOptionsLoaded', (itemName) =\u003e {\n    console.log(objectName, 'onOptionsLoaded', itemName);\n  });\n  // onUploadCancel\tfires when the user cancels uploading of a file (clicks the button ).\n  obj.attachEvent('onUploadCancel', (fileName) =\u003e {\n    console.log(objectName, 'onUploadCancel', fileName);\n  });\n  // onUploadComplete\tfires when all files from the list have been uploaded to the server\n  obj.attachEvent('onUploadComplete', (count) =\u003e {\n    console.log(objectName, 'onUploadComplete', count);\n  });\n  // onUploadFail\tfires when the file upload has failed\n  obj.attachEvent('onUploadFail', (fileName) =\u003e {\n    //fileName the real name of the file (as it's displayed in the control)\n    console.log(objectName, 'onUploadFail', fileName);\n  });\n  // onUploadFile\tfires when a single file from the list has been uploaded to the server\n  obj.attachEvent('onUploadFile', (clientFileName, serverFileName) =\u003e {\n    console.log(objectName, 'onUploadFile', clientFileName, serverFileName);\n  });\n  // onValidateError\tfires for each error during validation\n  obj.attachEvent('onValidateError', (name, value, result) =\u003e {\n    console.log(objectName, 'onValidateError', name, value, result);\n  });\n  // onValidateSuccess\tfires for each success during validation\n  obj.attachEvent('onValidateSuccess', (name, value, result) =\u003e {\n    console.log(objectName, 'onValidateSuccess', name, value, result);\n  });\n  // onXLE\tfires when the data loading is finished and a component or data is rendered\n  obj.attachEvent('onXLE', () =\u003e {\n    //callback order: onXLS event =\u003e [request] =\u003e onXLE event =\u003e doOnLoad()\n    console.log(objectName, 'onXLE');\n  });\n  // onXLS\tfires when XML loading started\n  obj.attachEvent('onXLS', () =\u003e {\n    //callback order: onXLS event =\u003e [request] =\u003e onXLE event =\u003e doOnLoad()\n    console.log(objectName, 'onXLS');\n  });\n}\n```\n\nupdate our main app so it binds the events\n\n`public/app.js`\n\n```javascript\n//...\nfunction contactsInit(cell) {\n  if (contactsLayout == null) {\n    // init layout\n    contactsLayout = cell.attachLayout(\"2U\");\n    contactsLayout.cells(\"a\").hideHeader();\n    contactsLayout.cells(\"b\").hideHeader();\n    contactsLayout.cells(\"b\").setWidth(330);\n    contactsLayout.cells(\"b\").fixSize(true, true);\n    contactsLayout.setAutoSize(\"a\", \"a;b\");\n\n    // attach grid\n    contactsGrid = contactsLayout.cells(\"a\").attachGrid();\n    contactsGrid.enableEditEvents(true,true,true);\n    contactsGrid.init();\n    contactsGrid.load(\"api/contacts?type=\" + A.deviceType, function () {\n      contactsGrid.selectRow(0, true);\n    }, \"json\");\n\n    attachDpGrid(contactsGrid, 'contactsGrid'); // \u003c\u003c\u003c\u003c\n\n    // attach form\n    contactsGrid.attachEvent(\"onRowSelect\", contactsFillForm);\n    contactsGrid.attachEvent(\"onRowInserted\", contactsGridBold);\n\n    contactsForm = contactsLayout.cells(\"b\").attachForm([\n      { type: \"settings\", position: \"label-left\", labelWidth: 110, inputWidth: 160 },\n      { type: \"container\", name: \"photo\", label: \"\", inputWidth: 160, inputHeight: 160, offsetTop: 20, offsetLeft: 65 },\n      { type: \"input\", name: \"name\", label: \"Name\", offsetTop: 20 },\n      { type: \"input\", name: \"email\", label: \"E-mail\" },\n      { type: \"input\", name: \"phone\", label: \"Phone\" },\n      { type: \"input\", name: \"company\", label: \"Company\" },\n      { type: \"input\", name: \"info\", label: \"Additional info\" }\n    ]);\n    attachDpForm(contactsForm, 'contactsForm'); // \u003c\u003c\u003c\u003c\n    contactsForm.setSizes = contactsForm.centerForm;\n    contactsForm.setSizes();\n  }\n}\n//...\n```\n![Form events showing in the console](tutorial_images/Screenshot_20180607_153040.png)\n\nLooking at the events, the `onChange` seems the most suitable to fire a request to the API.\n\nThis is what we have to do;\n\n- [ ] get all data to be send\n- [ ] send the data\n- [ ] sync with the grid \n\nGetting the `rowId` is not that hard: \n\n```javascript\nobj.getItemValue('id');\n```\n\nSending the request is the same as in the grid, but then we have a problem; how are we going to communicate back to grid within this function? Let me try to see if we can create a custom event:\n\n```javascript\nobj.callEvent(\"onAfterChange\", [rowId, itemName, value]);\n```\nSo in the script:\n\n`public/dhx-fetch-dp-form.js`\n\n```javascript\n// onChange\tfires when data in some input was changed\nobj.attachEvent('onChange', (itemName, value, state) =\u003e {\n  //state = checked/unchecked (for checkboxes and radios only)\n  console.log(objectName, 'onChange', itemName, value, state);\n  let rowId = obj.getItemValue('id');\n  let request = `{\"${itemName}\":\"${value}\"}`;\n  console.log(objectName, 'request', JSON.parse(request), rowId);\n  fetch(`/api/contacts/${rowId}`, {\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    method: 'PUT',\n    body: request\n  })\n    .then(response =\u003e response.json())\n    .then(response =\u003e {\n      console.log(objectName, 'response', response);\n      // call a custom event, onAfterChange doesn't exist\n      obj.callEvent(\"onAfterChange\", [rowId, itemName, value]);\n    })\n    .catch(err =\u003e { console.error(err) });\n});\n```\n\nIf that is possible we can easily 'catch' it again in the app.js, where all items are spaghetti-coded anyway:\n\n`pubilc/app.js`\n\n```javascript\ncontactsForm.attachEvent(\"onAfterChange\", (rowId, field, value) =\u003e {\n  fieldIndex = contactsGrid.getColIndexById(field);\n  console.log('contactsForm', 'onAfterChange', 'CUSTOM EVENT!', rowId, field, value, fieldIndex);\n  contactsGrid.cells(rowId, fieldIndex).setValue(value);\n});\n```\n\nWhat do you know, it works. You can check it out at [Brach Step4e](https://github.com/rkristelijn/dhtmlx-json-node/tree/Step4e)\n\n![Screenshot of updating the form: connected to the grid](tutorial_images/Screenshot_20180607_162339.png)\n\n## Step 4d: POST for create\n\n[back to top](#plan)\n\nWe take the same approach as the previous step (which is, design top-down, build bottom-up):\n\n- [x] create the API and test it\n- [x] connect the toolbar button\n- [x] complete interaction with the grid\n\nYou can see/clone the result of this chapter on [Branch Step4e](https://github.com/rkristelijn/dhtmlx-json-node/tree/Step4d) (sorry, messed up right order, but the hyperlink is right)\n\n`api/contacts/contacts-controller.js`\n\n```javascript\n//add following function\nlet _createOne = (data, callback) =\u003e {\n  Model.create(data, (err, contact) =\u003e {\n    if (err) callback(err, null);\n    else callback(null, contact);\n  });\n};\n//...\n// revealing model pattern, not revealing _toRows()\nreturn {\n  readAll: _readAll,\n  updateOne: _updateOne,\n  createOne: _createOne // \u003c\u003c\u003c\u003c add this line\n};\n```\n\n`api/contacts/contacts-router.js`\n\n```javascript\n//add following function\ncontactsRouter.post('/', (req, res) =\u003e {\n  contactsController.createOne(req.body, (err, contact) =\u003e {\n    if(err) {\n      res.sendStatus(400).end(err);\n    } else {\n      res.json(contact);\n    }\n  });\n});\n```\nTest using postman, payload:\n\n```json\n{\n\t\"photo\": \"\u003cimg src='imgs/contacts/small/remi-kristelijn.jpg' border='0' class='contact_photo' height='40' width='40'\u003e\",\n\t\"name\": \"Remi Kristelijn\",\n\t\"dob\": \"12/17/1079\",\n\t\"pos\": \"Enterprise Architect\",\n\t\"email\": \"rkristelijn@mail.com\",\n\t\"phone\":\"1-842-458-1452\",\n\t\"company\":\"Accenture\"\n}\n```\n\n![Postman showing POST method](tutorial_images/Screenshot_20180608_095753.png)\n\n```bash\nlistening on *:3000\nConnected to mongoose\nMongoose: contacts.insertOne({ _id: ObjectId(\"5b1a36c201c7e91842e32afc\"), photo: '\u003cimg src=\\'imgs/contacts/small/remi-kristelijn.jpg\\' border=\\'0\\' class=\\'contact_photo\\'\u003e', name: 'Remi Kristelijn', dob: '12/17/1079', pos: 'Enterprise Architect', email: 'rkristelijn@mail.com', phone: '1-842-458-1452', company: 'Accenture', created: new Date(\"Fri, 08 Jun 2018 07:56:50 GMT\"), __v: 0 })\nPOST / 200 43ms\n```\n\nNow let's connect that to the button;\n\nFirst I tried to add an event within the initialization of the contacts layout in function`contactsInit()`. I also added one in the `projectsInit()` like the one below.\n\n`public/app.js`\n\n```javascript\ncontactsInit(cell) { \n  // ...\n  mainToolbar.attachEvent(\"onClick\", (buttonId) =\u003e {\n    console.log(\"mainToolbar/contact\", \"onClick\", buttonId);\n  });\n  // ...\n}\n\nprojectsInit(cell) {\n  // ...\n  mainToolbar.attachEvent(\"onClick\", (buttonId) =\u003e {\n    console.log(\"mainToolbar/contact\", \"onClick\", buttonId);\n  });\n  // ...\n}\n```\n\nHowever when switching to projects and back, clicking the button resulted in both events fireing. So I needed to filter out to fire only when contacts was selected.\n\n`public/app.js`\n\n```javascript\ncontactsInit(cell) { \n  // ...\n  mainToolbar.attachEvent(\"onClick\", (buttonId) =\u003e {\n    if(mainSidebar.getActiveItem() === 'contacts') {\n        console.log(\"mainToolbar/contact\", \"onClick\", buttonId);\n        let rowId = contactsGrid.uid();\n        contactsGrid.addRow(rowId, \"\");\n        contactsGrid.selectRowById(rowId);\n      }\n  });\n  // ...\n}\n\nprojectsInit(cell) {\n  // ...\n  mainToolbar.attachEvent(\"onClick\", (buttonId) =\u003e {\n    if(mainSidebar.getActiveItem() === 'projects') {\n      console.log(\"mainToolbar/contact\", \"onClick\", buttonId);\n    }\n  });\n  // ...\n}\n```\nNow let's connect the API call.\n\n`/public/dhx-fetch-dp-grid.js`\n\n```javascript\n//fires right after a row has been added to the grid\n  obj.attachEvent('onRowAdded', (rId) =\u003e {\n    console.log(objectName, 'onRowAdded', rId);\n    fetch(`/api/contacts/`, {\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      method: 'POST'\n    })\n      .then(response =\u003e response.json())\n      .then(response =\u003e {\n        console.log(objectName, 'response', response);\n\n        /* NOTE: we need to replace the dhx-created (temporarely) rowId with the server row-id */\n        obj.callEvent(\"onAfterRowAdded\", [rId, response._id]);\n      })\n      .catch(err =\u003e { console.error(err) });\n  });\n```\n\nAs you can see we need another event to do something after the creation in the database. So we fire a custom event: `onAfterRowAdded` and catch that again in `app.js`\n\n`public/app.js`\n\n```javascript\ncontactsInit(cell) { \n  // ...\n  contactsGrid.attachEvent(\"onAfterRowAdded\", (tempRowId, serverRowId) =\u003e {\n      console.log('contactsGrid', 'onAfterRowAdded', 'CUSTOM EVENT!', tempRowId, serverRowId);\n      contactsGrid.changeRowId(tempRowId, serverRowId);\n    });\n  // ...\n}\n```\n\nNow there one thing that is impossible to populate now, the avatar image. Let's predefault that in the model.\n\n`/api/contacts/contacts-model.js`\n\n```javascript\nconst ContactsSchema = mongoose.Schema({\n  // ...\n  photo: {\n    type: String,\n    default: \"\u003cimg src='imgs/contacts/small/some-one.png' border='0' class='contact_photo' height='40' width='40'\u003e\"\n  },\n  // ...\n});\n```\nNow we can add new contacts.\n\n![Screenshot of the app, being able to add and update contacts](tutorial_images/Screenshot_20180608_120538.png)\n\n## Step 4f: DELETE for delete\n\n[back to top](#plan)\n\n`api/contacts/contacts-controller.js`\n\n```javascript\nlet _deleteOne = (id, callback) =\u003e {\n  Model.findByIdAndRemove({ _id: id }, (err) =\u003e {\n    if (err) callback(err, null);\n    else callback(null, null);\n  });\n};\n//...\n// revealing model pattern, not revealing _toRows()\nreturn {\n  readAll: _readAll,\n  updateOne: _updateOne,\n  createOne: _createOne,\n  deleteOne: _deleteOne // \u003c\u003c\u003c\u003c\n};\n// ...\n```\n\n`api/contacts/contacts-router.js`\n\n```javascript\ncontactsRouter.delete('/:id', (req, res) =\u003e {\n  contactsController.deleteOne(req.params.id, (err) =\u003e {\n    if (err) {\n      res.sendStatus(400).end(err);\n    } else {\n      res.sendStatus(204).end(req.params.id + \" removed\");\n    }\n  });\n});\n```\n\n`public/app.js`\n\n```javascript\nmainToolbar.attachEvent(\"onClick\", (buttonId) =\u003e {\n  if (mainSidebar.getActiveItem() === 'contacts') {\n    console.log(\"mainToolbar/contact\", \"onClick\", buttonId);\n    let rowId;\n    switch (buttonId) {\n      case \"add\":\n        rowId = contactsGrid.uid();\n        contactsGrid.addRow(rowId, \"\");\n        contactsGrid.selectRowById(rowId);\n        break;\n      case \"del\":\n        rowId = contactsGrid.getSelectedRowId();\n        let rowIndex = contactsGrid.getRowIndex(rowId);\n        contactsGrid.deleteRow(rowId);\n        // highlight the next record, or the previous record when deleting the last line\n        if (rowIndex \u003c contactsGrid.getRowsNum()) {\n          contactsGrid.selectRow(rowIndex, true);\n        } else {\n          contactsGrid.selectRow(rowIndex - 1, true)\n        }\n        break;\n    }\n  }\n});\n```\n\n`public/dhx-fetch-dp-grid.js`\n\n```javascript\n//fires after a row has been deleted from the grid\nobj.attachEvent('onAfterRowDeleted', (id, pid) =\u003e {\n  console.log(objectName, 'onAfterRowDeleted', id, pid);\n  fetch(`/api/contacts/${id}`, {\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    method: 'DELETE'\n  })\n    .then(response =\u003e {\n      console.log(objectName, 'response', response.statusText);\n    })\n    .catch(err =\u003e { console.error(err) });\n});\n```\n\n# References\n\n[back to top](#plan)\n\n\n## Markdown\n\n- [Markdown Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet)\n\n\n## Create a branch\n\n`git checkout -b step1`\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frkristelijn%2Fdhtmlx-json-node","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frkristelijn%2Fdhtmlx-json-node","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frkristelijn%2Fdhtmlx-json-node/lists"}