Хабрахабр

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

В данной статье будет описано как сделать фронтенд на 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 — микро шаблона для карточки товара.

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

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

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

В javascript:

var routes = , ["/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:

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

— далее название нужного нам свойства. В самом методе 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»] и при его наступлении удаляет все контейнеры и создает новые на основании данных с сервера.

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть