HTML шаблонизатор

В язык программирования встроена возможность писать HTML и CSS прямо в коде. Транслятор, автоматически транслирует код в php и js, так чтобы он работал на сервере и браузере, и одинаково отображал верстку. В JS также доступны обработчики события. Обновление HTML кода на клиенте происходит путем патчинга HTML дерева через RenderDriver.

Листинг кода приведен в конце.

Принцип работы

Чтобы отрисовать страницу нужна модель, и функция рендера. Модель хранит данные страницы.

Модель страницы - неизменяемая структура данных. Чтобы изменить страницу, нужно изменить модель, вызвав функцию this.updateModel(Dict map) или this.setModel(CoreStruct model) в обаботчике событий компонента. После этого будет создана новая модель, и отправлен запрос на перерисовку страницы.

Перерисовка страницы происходит через RenderDriver, используя requestAnimationFrame, путем вызова функции render у каждого компонента, который нужно перерисовать. В функции render происходить патч HTML дерева.

Данный метод не использует Virtual DOM, как это сделано, например, в React JS. Render Driver напрямую патчит HTML через функцию render компонента.

Точка входа

Функция static async RenderContainer IndexPage(RenderContainer container) является точкой входа в программу. @Route{ "uri": "/" } задает маршрут точку входа в функцию.

Данная функция создает модель IndexPageModel, и задает title для страницы.
f_inc - обновляет css и js, добавляет ?_=number к загружаемым css и js файлам.

CSS

CSS страницы описан в теге функции css(). Выражение %content означает, что будет сформирован content-f224, где f224 это хэш класса. Это нужно, чтобы одинаковые css селекторы разных классов не пересекались.
Чтобы прописать этот css в html теге нужно указать: <div @class='content'>

Render

Отрисовка HTML происходит в функции: render(LayoutModel layout, IndexPageModel model, Dict params, html content)

Параметры функции:

  • LayoutModel - модель шаблона
  • IndexPageModel - модель компонента
  • Dict params - параметры, которые были переданы в компонент
  • html content - контент, который был передан в компонент

К примеру, если вызвать компонент <Button kind='success' @event:MouseClickEvent='onMouseClick'>Save</Button>, то:
в params будет содержаться kind='success'
в content будет содержаться текст Save

Другой пример: <Dialog @bind="dialog" @ref="dialog" style="promt" @event:DialogEvent="onDialogEvent" />
Будет отрисован компонент Dialog.

Обработчики событий

Выражение @event:DialogEvent="onDialogEvent" и @event:MouseClickEvent='onMouseClick'это обработчики событий. Если Dialog вызовет событие DialogEvent, то будет вызвана функция onDialogEvent у родительского компонента, который вызвал Dialog. И соответственно onMouseClick, при нажатии мыши.

Существуют два типа событий. Синхронные и асинхронные.
Для синхронных нужно указывать @event:MouseClickEvent='onMouseClick'
Для асинхронных @eventAsync:MouseClickEvent='onMouseClickAsync'
Асинхронные позволяют делать аякс запросы к бэкенд.

Соответственно для асинхронных событий, обработчик будет выглядеть так: async void menuClick(MouseClickEvent e)

Код e.cancel(); позволяет отменить дефолтное JS событие в браузере.

Код:

this.updateModel
{
	"content": this.model.content ~ "!",
};

Вносит изменения в модель, и отправляет вышестоящему компоненту событие, о том, что модель изменилась. Далее по цепочке вверх передается событие, с измененными моделями компонентов, и RenderDriver получает измененную LayoutModel. После этого RenderDriver отправляет запрос на перерисовку, используя requestAnimationFrame.

Пример кода

namespace App.UI;

use Runtime.MessageRPC;
use Runtime.Web.Component;
use Runtime.Web.LayoutModel;
use Runtime.Web.RenderContainer;
use Runtime.Web.RenderHelper;
use Runtime.Web.Annotations.Route;
use Runtime.Web.Annotations.RouteList;
use Runtime.Web.Annotations.Template;
use Runtime.Web.Button.Button;
use App.Model.IndexPageModel;


@RouteList{}
@Template{ "model_name": classof IndexPageModel }
class IndexPage extends Component
{
	
	/**
	 * Returns module name
	 */
	lambda string moduleName() => "App";
	
	
	
	/**
	 * Route Action
	 * @return WebContainer
	 */
	@Route{ "uri": "/" }
	static async RenderContainer IndexPage(RenderContainer container)
	{
		IndexPageModel model = new IndexPageModel
		{
			"content": "Hello world",
		};
		
		/* Set title */
		container <= layout <= title <= "Hello world !!!";
		
		/* Create model */
		container <= layout <= page_class <= classof IndexPage;
		container <= layout <= page_model <= model;
		container <= layout <= f_inc <= 1;
		
		return container;
	}
	
	
	
	/**
	 * Returns required components
	 */
	lambda Collection components() =>
	[
		classof Button,
	];
	
	
	
	/**
	 * Component css
	 */
	lambda string css(Dict vars) =>
		@css{
			%content{
				text-align: center;
				padding-top: 50px;
			}
			%label{
				padding-bottom: 5px;
			}
			%input{
				padding: 5px 10px;
			}
		}
	;
	
	
	
	/**
	 * Component render
	 */
	lambda html render(LayoutModel layout, IndexPageModel model, Dict params, html content) =>
	
		<div @class='content' @key='content'>
			<div @class='label'>@{ model.content }</div>
			<input @class='input' @bind="content" </input>
			<Button @event:MouseClickEvent='onMouseClick'>Click Me!</Button>
		</div>
	;
	
	
	
	/**
	 * Mouse click event
	 */
	void onMouseClick(MouseClickEvent e)
	{
		e.cancel();
		this.updateModel
		{
			"content": this.model.content ~ "!",
		};
	}
	
	
}