https://habr.com/ru/post/458210/- JavaScript
- Программирование
В данной статье будет описано как сделать фронтенд на 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: "",
},
["emiter-variant-template"]: {///для смены шаблонов из трех вариантов который отображается в cardsingle в свойстве render
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]
<!-- 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" 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><!--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» — отображает текущий вариант шаблона для каждой карточки
data-cardsingle-listenvariant=«emiter-variant-template» — слушает какой вариант шаблон сейчас должен отображаться и меняет его в свойстве «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(){///отображает выбранный вариант
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 находится в тестовой версии, однако уже сейчас с помощью него можно решать многие типовые задачи фронтенд разработки.
Краткая документация по всем основным свойствам а также туториалы к некоторым примерам приложений можно почитать
здесь.