В данной статье будет описано как сделать фронтенд на 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