Построение таблицы данных GRID по JSON файлу с возможностью редактирования

Настало время рассмотреть пример приближенный к реальной задаче. Допустим, есть на сервере файл в JSON формате, который нужно форматировать через интерфейс. Для этого может пригодиться компонент в виде редактируемой таблицы GRID.

Итак, создадим компонент GRID для редактирования текстовых файлов в формате JSON

В этом компоненте бизнес- логика будет на PHP, ядровая часть на PHP, JQUERY, HTML, CSS

Проект состоит из файлов:

file.json - исходный файл с данными
file_grid.js - Java Script файл с вспомогательными функциями
file_grid.php - файл с бизнес логикой
lib.php - файл с библиотекой
utils.php - файл с общими вспомогательными функциями
jquery-3.7.1.min.js - файл библиотеки Jquery, который можно взять с jquery.com

Все файлы лежат в одной папке. Точкой входа в проект является файл file_grid.php.

При запуске отображается таблица, которую можно редактировать.

Исходное отображение таблицы
Исходное отображение таблицы

Любое значение таблицы можно поменять и сохранить в исходный файл. Войти в редактирование - двойной щелчок мыши, отменить изменение Ctrl+Z, запостить - сместить мышкой фокус на другую ячейку или область экрана. Сохранит данные на сервере - нажатие кнопки “Сохранить”.

Отображение inline редактора
Отображение inline редактора

При нажатии на кнопку “Сохранить” вызывается событие в PHP файле, в котором происходит сохранение и выдается сообщение “Сохранено!”.

Сообщение о сохранении данных на сервере
Сообщение о сохранении данных на сервере

Данные в исходном файле file.json:

[
  {
	"name": "Samsung",
	"price": "51201.62"
  },
  {
	"name": "Lg",
	"price": "5400.6"
  },
  {
	"name": "Alcatel",
	"price": "4503.6"
  }
]

Формат исходных данных представляет из себя набор записей с одинаковым количеством столбцов в каждой из них. 

Состав файла file_grid.js:

<?php

$(document) . ready(
	function() {
		$("body") . delegate(
			'[change_cell]',
			"change",
			function(event) {
				id = $(this) . attr('id');
				parentid = $(this) . attr('parentid');
				val = event . target . value;
				v = $('JSN') . text();
				jsn = JSON . parse(v);
				if (jsn) {
					jsn[parentid][id] = val;
				};
				s = JSON . stringify(jsn);
				$('JSN') . text(s);
			}
		);

		function getparams($attr)
		{
			req = '[';
			if ($attr) {
				jsn = JSON . parse($attr);
				var name;
				var param;
				var attrname;
				var value;
				for (var i = 0; i < jsn . length; i++) {
					name = jsn[i] . name;
					id = jsn[i] . id;
					param = jsn[i] . param;
					attrname = jsn[i] . attrname;
					var type = jsn[i] . type;
					if (type === "value") {
						if (name) value = $('[name='+name + ']') . val();
						if (id) value = $('[id='+id + ']') . val();
					}
					if (type === "attr") {
						if (name) value = $('[name='+name + ']') . attr(attrname);
						if (id) value = $('[id='+id + ']') . attr(attrname);
					}
					if (type === "text") {
						if (name) value = $('[name='+name + ']') . text();
						if (id) value = $('[id='+id + ']') . text();
					}
					if (type === "html") {
						if (name) value = $('[name='+name + ']') . html();
						if (id) value = $('[id='+id + ']') . html();
					}
					if (req !== '[') req = req + ',';
					req = req + '{"'+param + '":"'+encodeURI(value) + '"}';
				}
			}
			req = req + ']';
			return req;
		}

		$("body") . delegate(
			'[click]',
			"click",
			function() {
				req = getparams($(this) . attr('request'));

				$. post(
					'file_grid.php', {
						type:"event",
						name:$(this
					) . attr('click'), request:req
				},
				onSuccess
			);
		}
	);

	function onSuccess(data)
	{
		jsn = JSON . parse(data);
		if (!jsn . exec == '') {
			eval(jsn . exec);
		}
		if (!jsn . message == '') {
			alert(jsn . message);
		}
	}
});

В java-script файле file_grid.js реализовано делегирование события изменения ячейки change,  при котором записывает значение ячейки в JSON объект HTML формы. JSON объект хранится в теге JSN. Так же реализовано делегирование события click кнопки сохранения таблицы, из него вызывается событие в PHP файле, где и происходит это изменение. Вспомогательная функция getparams формирует JSON для передачи из JS скрипта в PHP событие.

Состав файла file_grid.php:

<?php

include('utils.php');

function event_click($data)
{
	$mes = 'Сохранено!';

	$text = urldecode(getval($data, 'jsn'));
	$file = urldecode(getval($data, 'file'));

	file_put_contents($file, $text);

	$jsn = json_encode(['result' => 'OK', 'message' => $mes]);

	return $jsn;
}

if (isset($_POST['type'])) {
	if ($_POST['type'] == 'event') {
		if (isset($_POST['name'])) {
			$request = array();
			if (isset($_POST['request'])) {
				$request = $_POST['request'];
			}
			echo call_user_func($_POST['name'], $request);
		}
	}
} else {
	include('lib.php');

	echo '<!DOCTYPE html>
	<html lang="en">
	<head>
        	<meta charset="UTF-8">
        	<title>edited grid</title>
        	<script src="jquery-3.7.1.min.js"></script>
        	<script src="file_grid.js"></script>
	</head>
	<body>';

	$e = new file_grid;
	$e->id = 'file_grid';
	$e->file = 'file.json';
	echo $e->print();

	echo '
	</body>
	</html>';
}

Файл file_grid.php служит для первоначальной отрисовки таблицы, а так же для обработки событий, которые вызываются из браузера. event_click - событие сохранения таблицы. call_user_func - стандартная функция PHP для CALLBACK вызова event_click. Переменная $e содержит экземпляр класса, который описан в файле lib.php. $e->print() - вывод на экран таблицы данных.

Состав файла lib.php:

<?php

define(
	'STYLE',
	'<style>
	grid{
  	border-top: 1px solid black;
  	border-left: 1px solid black;
  	display: table;
	}
	row{
  	display: table-row;  
	} 	 
	cell{
  	display: table-cell;
  	border-right: 1px solid black;  
  	border-bottom: 1px solid black;   	 
	}
	hrow{
  	display: table-row;  
	}  	 
	hcell{
  	background-color: grey;
  	color: white;  	 
  	display: table-cell;
  	border-right: 1px solid black;  
  	border-bottom: 1px solid black;   	 
	}  	 
	JSN{
  	display: none;
	}
 </style>'
);
define('CONTENT', '%content%');
define('ID', '%id%');
define('PARENTID', '%parentid%');
define('ELEMENT', '<div id=' . ID . ' >' . CONTENT . '</div>');
define(
	'CELL',
	'<cell name=cell_' . PARENTID . '_' . ID . ' '
		. 'id=' . ID . ' parentid=' . PARENTID . ' '
		. 'change_cell="" '
		. 'ondblclick=" '
		. "elem1 = $('[name=cell_" . PARENTID . "_" . ID . "]'); "
		. 'cont = elem1.text(); '
		. 'elem1.empty(); '
		. 'elem1.append(&quot;<textarea '
		. 'name=textarea_' . PARENTID . '_' . ID . ' '
		. "onfocusout=\&quot; "
		. "elem1 = $('[name=cell_" . PARENTID . "_" . ID . "]'); "
		. "elem2 = $('[name=textarea_" . PARENTID . "_" . ID . "]'); "
		. 'cont = elem2.val(); '
		. 'elem1.empty(); '
		. 'elem2.remove(); '
		. 'elem1.append(cont); '
		. '\&quot;>&quot;+cont+&quot;</textarea>&quot;); '
		. 'elem2 = $(&quot;[name=textarea_' . PARENTID . '_' . ID . ']&quot;); '
		. 'elem2.focus(); '
		. '" '
		. '>' . CONTENT . '</cell>'
);
define('ROW', '<row id=' . ID . ' >' . CONTENT . '</row>');
define('HCELL', '<hcell>' . CONTENT . '</hcell>');
define('HROW', '<hrow id=' . ID . ' >' . CONTENT . '</hrow>');
define('GRID', STYLE . '<grid id=' . ID . ' >' . CONTENT . '</grid>');
define('SAVE_CONTENT', 'Сохранить');
define('JSN', '%jsn%');
define('JSN_NAME', 'JSN_NAME');
define('JSN_CONT', '<JSN name=' . JSN_NAME . '>' . JSN . '</JSN>');
define(
	'BUTTON_SAVE',
	'<button name=button_save click=event_click '
		. 'request=[{"type":"text","name":"' . JSN_NAME . '","param":"jsn"},'
		. '{"type":"attr","attrname":"file","id":"' . ID . '","param":"file"}] '
		. '>' . SAVE_CONTENT . '</button>'
);
define('FILE_NAME', '%file_name%');
define('FILE_GRID', STYLE . '<grid id=' . ID . ' file= ' . FILE_NAME . ' >' . CONTENT . JSN_CONT . BUTTON_SAVE . '</grid>');

class element
{
	public $template;
	public $content;
	public $child;
	public $id;
	public $hasparent;
	public $parentid;
	public function __construct()
	{
		if (!isset($this->template)) {
			$this->template = ELEMENT;
		}
		$this->content = array();
	}
	function getclass()
	{
		if (isset($this->child)) {
			return new $this->child;
		} else {
			return new element;
		}
	}
	public function print($content = '') {
		if (is_array($this->content)) {
   		 //$content = '';
			foreach ($this->content as $k => $value) {
				$e = $this->getclass();
				$e->content = $value;
				$e->id = $k;
				if ($e->hasparent) {
					$e->parentid = $this->id;
				}
				$content .= $e->print();
			}
		} else {
			$content = $content . $this->content;
		}
		$ret = str_replace(
			array(ID, CONTENT, PARENTID),
			array($this->id, $content, $this->parentid),
			$this->template
		);
		return $ret;
	}
}

class cell extends element
{
	public function __construct()
	{
		$this->template = CELL;
		$this->hasparent = true;
		parent::__construct('');
	}
}

class row extends element
{
	public function __construct()
	{
		$this->template = ROW;
		$this->child = 'cell';
		parent::__construct();
	}
}

class hcell extends element
{
	public function __construct()
	{
		$this->template = HCELL;
		$this->hasparent = true;
		parent::__construct('');
	}
}

class hrow extends element
{
	public function __construct()
	{
		$this->template = HROW;
		$this->child = 'hcell';
		parent::__construct();
	}
}

class grid extends element
{
	public $header;
	public function __construct()
	{
		$this->template = GRID;
		$this->child = 'row';
		parent::__construct();
	}
	public function print($content = '') {
		$e = new hrow;
		$e->content = $this->header;
		$content = $e->print();
		return parent::print($content);
	}
}

class file_grid extends grid
{
	public $file;
	public $json;
	public function __construct()
	{
		parent::__construct();
		$this->template = FILE_GRID;
	}
	public function print($content = '') {
        	//$this->header = array('1'=>'column1','2'=>'column2');
		$this->json = file_get_contents($this->file);
		$arr = json_decode($this->json, true);
		foreach ($arr as $key => $value) {
			foreach ($value as $vkey => $vvalue) {
				$this->header[] = $vkey;
			}
			break;
		}
		$this->content = $arr;
		$ret = parent::print('');
		$ret = str_replace(
			array(JSN, FILE_NAME),
			array($this->json, $this->file),
			$ret
		);
		return $ret;
	}
}

В файле lib.php описаны базовые константы через стандартную функцию defile, а так же описана иерархия классов. Базовым классом является ELEMENT. Далее от него наследуется ROW, CELL, GRID. От GRID наследуется FILE_GRID. В каждом классе прописана своя специфическая логика. В константах задана как HTML, CSS разметка, так и события JAVA SCRIPT, такие как DBLCLICK и FOCUSOUT. Классы используют эти константы чтоб отрисовывать в статике и динамике HTML разметку.

Состав файла utils.php:

<?php

function getval($data, $name){    
	$rjsn = json_decode($data, true);
	foreach ($rjsn as $vjsn){
    	foreach ($vjsn as $key => $value) {
        	if($name == $key){
            	return $value;
        	}    
    	}	 
	}	 
	return 'undefined';
}

Функция getval используется для получения значения параметра по имени внутри PHP события.

Таким образом реализована редактируемая табличная форма данных, получаемая из JSON файла.