Создание приложения на Htmlix с роутером на серверной и клиентской стороне

в 9:19, , рубрики: javascript, javascript frontend framework, Программирование

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

  • первая это список из 6 товаров найденный в поиске по категориям;
  • вторая это сам товар по которому кликнули, товар будет состоять из основного шаблона и трех вариантов дополнительных шаблонов, которые будут выбираться в зависимости от категории и id карточки.

Код всего приложения можно скачать: здесь, все что в папке router,
а также файл app.js относится к нашему приложению.

Покликать похожий вариант (без серверной части) можно здесь: здесь.

Для тех кто не знаком с Htmlix, можно почитать более легкий для понимания материал здесь,

Файлы index.pug, card.pug и папка includes это то что сервер отдаст в первом запросе к нему
если localhost:3000/ или localhost:3000/categories/category(num) — отдаст index.pug, если запрос будет localhost:3000/cards/card?id=(num) — отдаст card.pug с одним файлом в папке includes в качестве под шаблона, который он выберет исходя из category_id (номера категории).

Далее уже из клиентской части приложение «догрузит» в fetch запросе один вариант шаблонов из папки template, если адрес был localhost:3000/categories/category(num) загрузит файл card.html, если запрос был localhost:3000/cards/card?id=(num) загрузит cards.html, а также в любом случае загрузит один вариант из папки json, в зависимости от того какая категория у нас сейчас выделена (на которой стоит класс ".hover-category")

На серверной стороне у нас будет express.js и шаблонизатор pug, серверная сторона в данной статье описываться практически не будет, все что нам о ней нужно знать это то что при запросе localhost:3000/ — нам выдаст список товаров из первой категории (6 шт.), при запросе localhost:3000/categories/category(num) — нам выдаст товары из num — категории (всего 4 категории начиная с 1), а при запросе localhost:3000/cards/card?id=(num) нам выдаст саму карточку товара по номеру id (всего может быть 6 номеров начиная с 0) если num категории либо товара еще не создан выдаст страницу 404.

Все приложение у на будет состоять из компонентов, и в зависимости от маршрута в url будет показываться один компонент и скрываться другой, всего будет 6 компонентов: categories, cards, cardsingle, variants1, variants2, variants3 из них categories это левая сторона экрана со списком категорий — видна на всех адресах url, cards — список отфильтрованных карточек товара виден только на адресах -localhost:3000/ и localhost:3000/categories/category(num) и cardsingle — карточка товара по которой кликнули, показывает дополнительную информацию, а также один из вариантов variants1, variants2, variants3 — варианты мкро шаблона для карточек товара.

Чтобы не писать различный код для разных вариантов маршрута, наше приложение с помощью роутера определит какой сейчас маршрут и загрузит в первую очередь те компоненты которые должны отображаться на данном этапе, а остальные загрузит с template с помощью fetch запроса. Например если сейчас маршрут localhost:3000/categories/category(num) то первыми будут инициализированы компоненты: categories и cards а если localhost:3000/cards/card?id=(num) то categories, cardsingle и один вариант из под шаблонов в зависимости от id- категории, например variants2.

Для того чтобы указать какие компоненты загружать первыми а какие остальными, а также сообщить при каком роуте какой компонент скрывать а какой показывать необходимо создать обьект routes, и передать его вместе с описанием приложения Stste в функцию HTMLixRouter(State, routes), создадим обьект routes:

В html коде роутер указывается добавлением data-router=«router» в div в котором будет меняться представление.

В javascript:

var routes = {
	
	   ["/"]: {
		
		 first: ["cards", "categories"], // компонетты которые будут инициализированы в первую очередь
		routComponent: "cards", //компонент соответствующий данному роуту
		templatePath: "/router/template/card.html" // папка для загрузки шаблонов
	},	
	
	["/categories/category*"]: { //знак * - говорит что /categories/category(num) - тоже подойдет, если не указать будет искать точное совпадение
		
		first: ["cards", "categories"], 
		routComponent: "cards", 
		templatePath: "/router/template/card.html" 
	},
	
	["/cards/card*"]: {
		
		first: ["cardsingle", "categories"],
		routComponent: "cardsingle",
		templatePath: "/router/template/cards.html"
	},	
}

Далее необходимо создать все компоненты, в html, pug и javascript файлах

Для начала создадим структуру приложения в javascript файле router.example.js:


var State = {//описание приложения

	categories: {// компонент - массив категории
		
		container: 'categori',//название контейнеров компонента
		props: [/*здесь будет список всех свойств контейнера*/],
		methods: {
		        //здесь будут все методы контейнера 	
		},	
	},
	cards:{//компонент - массив -список карточек отфильтрованных товаров		
		container: 'card',// контейнер компонента
		arrayProps: [/*здесь будут свойства массива cards*/],
		arrayMethods: {
			//здесь будут методы массива 	
		},
		props: [/*здесь будут свойства контейнеров   'card' */],
		methods: {
                        ///здесь будут методы контейнеров   'card' 
		}
		
	},
	cardsingle: {//компонент - контейнер - текущая карточка товара для отображения при клике
		
		container: 'cardsingle',//название у контейнера тоже что и у компонента, т.к. он не находится в массиве

		props: [/*здесь список свойств контейнера*/],
		methods: {
			//здесь список методов контейнера			
		},
	},
	variants1: {//компонент массив  вариант, будет отображен в компоненте cardsingle в свойстве "render"
			container: "variant1",//название контейнеров
			props: [/**/],
			methods: {

				},		
			}			
	},///далее еще два компонента один- контейнер и второй- массив из контейнеров
	variants2: { {//компонент контейнер 
			container: "variants2",
			props: [],
			methods: {
				},
			
			},			
		},
		variants3: { 
			container: "variant3",
			props: [],
			methods: {
				}			
			},			
		},				
       //Создаем пользовательские события 
	eventEmiters: {
			

			["emiter-single-id"]: {//текущее id карты которая показывается в компоненте cardsingle
				
				prop: "0"
			},
			["emiter-fetch-posts"]: {///наступит при клике по категории и загрузки новых данных с сервера
				
				prop: "",
				
			},
			["emiter-click-category"]: {///наступит при клике по категории 
				
				prop: 0,
			},
			["emiter-chose-variant"]: {///наступит при клике на выбранном варианте в одном из вариантов шаблона
				prop: "",
			},
			["emiter-variant-template"]: {///для смены шаблонов из трех вариантов который отображается в cardsingle
				
				prop: "variants",
			}
			
	},
	stateMethods: {
		
		fetchPosts: function(nameFile, callb){
		///здесь будет метод для загрузки json файлов по имени файла nameFile и вызов callb при загрузке. 	
		},
		
	},

Теперь более подробно, создадим компоненты:

Компонент categories мы не будем «догружать» (он присутствует на всех адресах роутера)
поэтому он будет присутствовать только в pug — при первой отдаче файлов с сервера

-var categori_rout = "/categories/";
-var category_name = ["category1", "category2", "category3", "category4"]
	|
	ul(data-categories="array")
	      each val, index in category_name
		    li(data-categori="container" data-categori-clickcategory="click")								
			a(href=categori_rout+category_name[index]
		        class=index==category_id? "hover-category" : '' 									
			data-categori-listenclick="emiter-click-category" 
			data-categori-categoryclass="class" 
			data-categori-category_href="href")= category_name[index]

В коде выше мы создали список из категорий с помощью шаблонизатора, и указали класс «hover-category» той категории чей номер будет в строке запроса, а также обозначили все свойства, которые нам понадобятся в javascript:

data-categories=«array» ссылка на сам компонент categories;
data-categori=«container» ссылка к контейнерам компонента;
data-categori-clickcategory=«click» — свойство — слушатель события «click»;
data-categori-listenclick=«emiter-click-category» — свойство слушатель пользовательского события «emiter-click-category» для того чтобы убрать с себя класс «hover-category» при клике на другой категории
data-categori-categoryclass=«class» — свойство — доступ к классам внутри данной категории;
data-categori-category_href=«href» — свойство — доступ к атрибуту «href»

Теперь создадим данный компонент в javascript:


categories: {//название массива компонента
		
		container: 'categori', //название контейнеров 

		props: ["clickcategory", "listenclick",  "categoryclass", "category_href"], //перечисляем все свойства контейнера

		methods: {//все методы для свойств слушателей событий
			
			clickcategory: function(event){//кликнули по категории
				
				event.preventDefault();
			///получаем category_href в соседнем свойстве.ю в общем контейнере	
			var href = this.parentContainer.props.category_href.getProp();
			
                      ///устанавливаем новый маршрут в истории а также меняем компонент, который сейчас видно на странице
			this.rootLink.router.setRout(href);//устанавливаем следующий маршрут передав путь ссылки category_href 
			
			
				var nameFile= href.split("/").slice(-1)[0]//.split(".")[0]; //поиск имени файла из href без расширения 

				
				
				var eventProp = this.rootLink.eventProps["emiter-fetch-posts"]; 
				
				//вызываем пользовательское событие "emiter-click-category" и передаем id контейнера 
				this.rootLink.eventProps["emiter-click-category"].setEventProp(this.parentContainer.id)
				
                              //загружаем новые карточки товара, соответствующие нашему фильтру категорий,  после загрузки вызываем "emiter-fetch-posts" с новыми данными для обновления интерфейсы компонента cards

				this.rootLink.stateMethods.fetchPosts( nameFile,
													   function(jsonData){ 
													   
													 //  console.log(jsonData);
													   eventProp.setEventProp(jsonData)}
													 );
													 			
			},
			listenclick: function(){//слушаем событие "emiter-click-category" и берем из него переданный в методе выше id если он не соответствует нашему убираем класс  "hover-category"
				
				if(this.parentContainer.id == this.emiter.prop){
					
					this.parentContainer.props.categoryclass.setProp("hover-category");
				}else{
					
					this.parentContainer.props.categoryclass.removeProp("hover-category");
				}
				
			}
			
		},
	
	},

Далее создадим компонент cards — это массив из контейнеров который отображает список согласно данным фильтра (json), он может отдаваться с сервера при первом запросе, а может в fetch запросе в зависимости от текущего url поэтому он будет в файле index.pug и файле /template/cards.html Для большей простоты разберем как он выглядит в html файле:


<div class=" row" data-cards="array" data-cards-listenfetch="emiter-fetch-posts" data-cards-listenrout="emiter-router"><!-- компонент - массив cards -->
							
	<div data-card="container" class="col-4 card-in"><!--первый контейнер -->
		<h5 data-card-title="text">Название 1</h5>
		<a data-card-click="click" data-card-href="href" href="/cards/card?id=0">
			<img data-card-srcimg="src" src="../../img/images.jpg" />
		</a>
		<p data-card-paragraf="text">Краткое описание 1</p>
	</div><!-- первый контейнер -->

</div>

В шаблоне не обязательно указывать много контейнеров в массиве, т.к. для создания шаблона берется только первый для клонирования, остальные остаются без внимания.

Далее javascript код:


cards:{
		
		container: 'card',
		arrayProps: ["listenfetch"],
		arrayMethods: {
			
			listenfetch: function(){//метод для слушает событие ["emiter-fetch-posts"] и при его наступлении очищает массив и формирует новый на основании полученных данных 

				var newArray = this.emiter.prop;

				this.rootLink.clearContainer(this.pathToContainer);
				
			for(var i =0; i< newArray.length; i++){
					
			///создаем контейнеры в цикле указав им данные полученные с сервера 	
			var container =	this.rootLink.createContainerInArr(this.pathToContainer, {
						
					title: newArray[i].title,
					paragraf: newArray[i].paragraf_short,
					href: newArray[i].href,
					srcimg: newArray[i].srcimg
						
				});

				}
				this.rootLink.stateProperties.cards =  newArray;
                                ///меняем значение переменной в которой хранится информация о выборке с актуальными даннными
			}
			
		},
		props: ['title','paragraf',"click", 'srcimg', "href"], //теперь создаем свойства для контейнеров внутри массива
		methods: {
			
			click: function(event){//при клике на контейнере мы берем href атрибут, из него id карты для отображения и запускаем метод this.rootLink.router.setRout в который передали новую будущюю историю а также компонент для текущего отображения(можно не передавать), 
тогда роутер сравнит историю со всеми возможными компонентами и покажет нужный
				
				event.preventDefault();
				var href = this.parentContainer.props.href.getProp();
				var cardId = href.split("?")[1].split("=")[1];
				
				var oldHref = window.location.href;
				
				this.rootLink.router.setRout(href, this.rootLink.state["cardsingle"]);

				///вызвали пользовательское событие чтобы обновить данные в cardsingle
				this.rootLink.eventProps["emiter-single-id"].setEventProp([cardId, oldHref]);
			}
			
		}
		
	},

Далее создадим компонент cardsingle это контейнер без массива в котором показывается карточка при клике на нее, он также будет в card.pug если первый запрос к серверу срузу к карте и в template/card.html если мы его «догрузим» в fetch запросе.

Здесь также для простоты разберем только html вариант:

<div data-cardsingle="container" data-cardsingle-listenid="emiter-single-id"   class="card-single">
	<div class="row">
		<div class="col-7 card-left-column">
			<h5 data-cardsingle-title="text">Название</h5>
								
			<img data-cardsingle-srcimg="src" src="../../img/Thumbnail_300x300.png" />
								
			<p data-cardsingle-paragraf="text">Полное Описание</p>	
			<p >Категория: <span data-cardsingle-category="text">category 1</span></p>	
			<a data-cardsingle-clickback="click" data-cardsingle-href_back="href" href="/">< Назад</a>
		</div>
		<div class="col-5 right-columt">
			<div data-cardsingle-render="render-variant" data-cardsingle-listenvariant="emiter-variant-template">
                                     <!--- сюда подставится вариант шаблона -->
			</div>
		        <p >Вы выбрали : <span data-cardsingle-listenchosevariant="emiter-chose-variant" data-cardsingle-chosetext="text" style="color: red;"><span></p>
		</div>
								</div>	
</div>

В нем свойства:

data-cardsingle=«container» — ссылка на контейнер;
data-cardsingle-listenid=«emiter-single-id» — свойство слушатель пользовательского события;
data-cardsingle-title=«text» свойство — доступ к названию карточки
data-cardsingle-srcimg=«src» — адрес картинки
data-cardsingle-paragraf=«text» — текст полного описания
data-cardsingle-category=«text» — из какой категории
data-cardsingle-clickback=«click» — клик по кнопке «назад»
data-cardsingle-listenchosevariant=«emiter-chose-variant» — слушает какой вариант из списка выбран и отображает его
data-cardsingle-render=«render-variant» — отображает текущий вариант шаблона для каждой карточки
data-cardsingle-listenvariant=«emiter-variant-template» — слушает какой под шаблон сейчас должен отображаться

Далее javascript:

cardsingle: {//название компонента
		
		container: 'cardsingle', //название контейнера компонента

		props: ["render", "category", "title","srcimg", "paragraf", "href_back", "clickback",  "listenid", "listenchosevariant","listenvariant", "chosetext"],//перечень всех свойств
		methods: {
			

			clickback: function(event){//кнопка назад меняет роут а сответственно и вид
				
				event.preventDefault();

				var href = this.parentContainer.props.href_back.getProp();
				
				this.rootLink.router.setRout(href);

				
			},
			listenchosevariant: function(){///отображает выбранный вариант 
				
				this.parentContainer.props.chosetext.setProp(this.emiter.prop);
			},
	listenid: function(){//слушает событие "emiter-single-id" и изменяет свои свойства на основании полученных данных
				
		var id = this.emiter.prop[0];///получаем id выбранного элемента 
		var href = this.emiter.prop[1];
				
		var cards = this.rootLink.stateProperties.cards;

		this.parentContainer.props.title.setProp(cards[id].title);
		this.parentContainer.props.paragraf.setProp(cards[id].paragraf);
		this.parentContainer.props.href_back.setProp(href);
		this.parentContainer.props.srcimg.setProp(cards[id].srcimg);
		this.parentContainer.props.category.setProp(cards[id].category);
		this.parentContainer.props.chosetext.setProp("");
						
		              //вызываем событие для смены под шаблона на основе полученных данных
		this.rootLink.eventProps["emiter-variant-template"].setEventProp(cards[id].variant_template);
				
                         //если тип массив то формируем под шаблон на основе полученных данных
		if(this.rootLink.state[cards[id].variant_template].type== "array"){
					
		this.rootLink.clearContainer(cards[id].variant_template);
					
			for(var i =0; i< cards[id].variants.length; i++){
					
				this.rootLink.createContainerInArr(cards[id].variant_template, {
						
					text: cards[id].variants[i],
					
						
				});
			}					
		}				
	},///здесь слушаем "emiter-variant-template" вариант шаблона и меняем его с помощью универсального метода  .render.setProp(variant)
			listenvariant: function(){
				
				var variant = this.emiter.prop;
				
				this.parentContainer.props.render.setProp(variant);	
			}
			
			
		},
	},

Далее по тому же принципу создаем три варианта микро шаблонов для карточки товаров

<ul data-variants1="array">
	
	<li data-variant1="container" data-variant1-clickvariant="click" ><a  data-variant1-text="text" href="/">Вариант №1</a></li>
</ul>																
<form data-variants2="container">
	<div class="form-group">
		<label for="exampleFormControlSelect1">Выберите вариант:</label>
		<select data-variants2-clickvariant2="click" data-variants2-select="select" class="form-control" id="exampleFormControlSelect1">
			<option>1</option>
			<option>2</option>
			<option>3</option>
			<option>4</option>
			<option>5</option>
		</select>
   </div>
</form>
<form data-variants3="array">
<div data-variant3="container"  class="form-check">
  <input data-variant3-clickvariant="click"  class="form-check-input" type="radio" name="exampleRadios" id="exampleRadios1" value="option1" checked>
  <label data-variant3-text="text"  class="form-check-label" for="exampleRadios1">
    Вариант 1
  </label>
</div>	

Javascript:


	variants1: {
			container: "variant1",
			props: ["clickvariant", "text"],
			methods: {
			    clickvariant: function(event){
				event.preventDefault();
				this.rootLink.eventProps["emiter-chose-variant"].setEventProp(this.parentContainer.props.text.getProp());
				
				},		
			}			
	},
	variants2: { 
			container: "variants2",
			props: ["clickvariant2", "select"],
			methods: {
			    clickvariant2: function(event){
				event.preventDefault();
				this.rootLink.eventProps["emiter-chose-variant"].setEventProp(this.parentContainer.props.select.getProp());

				},
			
			},			
		},
		variants3: { 
			container: "variant3",
			props: ["clickvariant", "text"],
			methods: {
			    clickvariant: function(event){

				this.rootLink.eventProps["emiter-chose-variant"].setEventProp(this.parentContainer.props.text.getProp());
				
				}			
			},			
		},	

Методов в фремворке пока не очень много, с ними можно познакомиться посмотрев исходный код где есть краткое описание к основным используемым в ходе работы. Пока что htmlix находится в тестовой версии, однако уже сейчас с помощью него можно решать многие типовые задачи фронтенд разработки.

Краткая документация по всем основным свойствам а также туториалы к некоторым примерам приложений можно почитать здесь.

Автор: Mihail127

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js