В данной статье будет описано как сделать фронтенд на 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 — карточка товара по которой кликнули видна на localhost:3000/cards/card?id=(num), показывает дополнительную информацию, а также один из вариантов 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"
	},	
}

То есть в зависимости от адреса нам отдаются компоненты, а остальные мы «догружаем» с папки шаблонов template в fetch запросе и инициализируем их сразу после первых.

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

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

/* Напомню что элемент может являться либо контейнером (одиночным элементом), либо массивом из контейнеров, если контейнер в массиве, то его можно удалить либо добавит новый, если контейнер является одиночным элементом, его можно только изменить либо скрыть*/

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

	categories: {// компонент - массив cо ссылками на категории товара
		
		container: 'categori',//название контейнеров содержащихся в данном массиве (контейнер это одна ссылка со всеми свойствами)

		props: [/*здесь будет список всех свойств контейнера*/],
		methods: {
		        //здесь будут все методы контейнера 	
		},	
	},
	cards:{//компонент - массив -список карточек отфильтрованных товаров		
		container: 'card',// контейнер компонента
		arrayProps: [/*здесь будут свойства массива cards (свойства div элемента который содержит все карточки товара)*/],
		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: {
				}			
			},			
		},				
       //Создаем пользовательские события, для изменения состояния приложения
/*доступ к пользовательским событиям и их данным из слушателя this.emiter.prop, из любой точки приложения - this.rootLink.eventProps["emiter- название события"] далее либо getEventProp() либо setEventProp(новые данные)*/

	eventEmiters: {
			
			["emiter-single-id"]: {//текущее id карты которая показывается в компоненте cardsingle
				
				prop: "0"
			},
			["emiter-fetch-posts"]: {///наступит при клике по категории и загрузке новых данных с сервера
				
				prop: "",
				
			},
			["emiter-click-category"]: {///наступит при клике по категории 
				
				prop: 0,
			},
			["emiter-chose-variant"]: {///наступит при клике на выбранном варианте в одном из вариантов шаблона
				prop: "",
			},			
	},
	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]

<!-- Html вариант данного кода выглядел бы так -->

<ul data-categories="array"><!-- массив -->

<li data-categori="container" data-categori-clickcategory="click"><!--контейнер №1 -->
	<a data-categori-listenclick="emiter-click-category" data-categori-categoryclass="class" data-categori-category_href="href" class="hover-category" href="/htmlix_examples/router/category/category1.html">category1</a>
</li><--"hover-category" указывает на то что данная категория является текущей -->

<li data-categori="container" data-categori-clickcategory="click"><!--контейнер №2 -->
	<a data-categori-listenclick="emiter-click-category" data-categori-categoryclass="class" data-categori-category_href="href" href="/htmlix_examples/router/category/category2.html">category2</a>
</li>

<li data-categori="container" data-categori-clickcategory="click"><!--контейнер №3 -->
	<a data-categori-listenclick="emiter-click-category" data-categori-categoryclass="class" data-categori-category_href="href" href="/htmlix_examples/router/category/category3.html">category3</a>
</li>

<li data-categori="container" data-categori-clickcategory="click"> <!--контейнер №4 -->
	<a data-categori-listenclick="emiter-click-category" data-categori-categoryclass="class" data-categori-category_href="href" href="/htmlix_examples/router/category/category4.html">category4</a>
</li>

</ul><!--конец массива categories -->




В коде выше мы создали список из категорий с помощью шаблонизатора, и указали класс «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:

Основная функция данного компонента это поиск в атрибуте href имени категории и передача его в метод fetchPosts ( this.stateMethods.fetchPosts( nameFile, ...) ) для загрузки json выборки данной категории. (nameFile — совпадает с именем категории)

В самом методе clickcategory — this указывает на свойство обработчик события, для перехода к другим свойствам данного контейнера нужно вызвать this.parentContainer.props. — далее название нужного нам свойства.


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

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

	methods: {//все методы для свойств слушателей событий
			
	    clickcategory: function(event){//кликнули по категории
				
		    event.preventDefault(); //убрали переход по ссылке

			
		    var href = this.parentContainer.props.category_href.getProp();
                       ///получаем category_href в соседнем свойстве в общем контейнере	
			

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

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

		this.rootLink.stateMethods.fetchPosts( nameFile,  function(jsonData){ 
													   
																		 
                                  eventProp.setEventProp(jsonData) }
				);

				//вызываем пользовательское событие "emiter-click-category" и передаем id контейнера  для метода listenclick в этом-же компоненте
			this.rootLink.eventProps["emiter-click-category"].setEventProp(this.parentContainer.id)
				
													 			
		},
		listenclick: function(){//метод для снятия  "hover-category" если категория не соответствует текущей (выбранной), либо для его установки

//слушаем событие "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), он может отдаваться с сервера при первом запросе если url = localhost:3000/ или localhost:3000/categories/category(num), а может быть «дозагружен» 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>

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

Далее javascript код:
Здесь есть общий метод для всего массива arrayMethods — listenfetch который слушает событие [«emiter-fetch-posts»] и при его наступлении удаляет все контейнеры и создает новые на основании данных с сервера.

А также метод для контейнеров это click при клике на контейнер вызывает роутер и передает в него новый маршрут, а роутер на основании маршрута смотрит какой компонент скрыть, а какой показать, в нашем случае скроется компонент cards и покажется компонент «cardsingle», также мы в этом методе вызываем событие [«emiter-single-id»]. в которое передаем новые данные cardId и oldHref чтобы компонент «cardsingle» обновил свое представление на основании их и показал карточку соответствующую переданному в событии id.


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;
                                ///меняем значение переменной в которой хранится информация о выборке с актуальными даннными
		   }//конец метода listenfetch
			
		},
		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/Thul_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">

                         <!--- сюда подставится вариант шаблона -->

		</div>
		 <p >Вы выбрали : 
                      <span data-cardsingle-listenchosevariant="emiter-chose-variant" data-cardsingle-chosetext="text" style="color: red;">
                      <span>
                  </p>
	</div>

    </div><!--row -->	
</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» — слушает какой вариант из списка выбран и отображает его в свойстве chosetext
data-cardsingle-render=«render-variant» — отображает текущий вариант шаблона для каждой карточки

Далее javascript:
основной метод это listenid который слушает событие «emiter-single-id» получает переданное в событие id элемента по которому кликнули и на основании его берет данные из соответствующего json обьекта и обновляет все свои свойства, тем самым обновив представление, а также обновляет вариант своего микрошаблона.

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(){///слушает событие "emiter-chose-variant" и отображает выбранный вариант 
				
				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("");
						
                         //если тип - массив, то формируем под шаблон на основе полученных данных
		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],
					
						
				});
			}
                    ///отображаем соответствующий микрошаблон 
                   this.parentContainer.props.render.setProp(cards[id].variant_template);					
		}				
	},

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

<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>
	
</form>

Javascript:


	variants1: {
			container: "variant1",
			props: ["clickvariant", "text"],
			methods: {
			    clickvariant: function(event){
				event.preventDefault();

                               /*берем данные из клика по варианту и отправляем их в событие "emiter-chose-variant" чтобы метод listenchosevariant компонента cardsingle обновил свое представление */
				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 находится в тестовой версии, однако уже сейчас с помощью него можно решать многие типовые задачи фронтенд разработки.

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

Комментарии (2)


  1. JSMonk
    30.06.2019 12:45
    +1

    Поправьте, пожалуйста, форматирование — код тяжко воспринимается.


    1. Lodin
      30.06.2019 23:45

      К сожалению, автор не воспринимает рекомендации. Ему уже не раз и про дикое форматирование писали, и про то, что фреймворк без тестов и npm — это нонсенс в 2к19, бесполезно.