Русский
Русский
English
Статистика
Реклама

Javascript framework

Из песочницы Создание прототипа SPA интернет магазина на htmlix.js

13.06.2020 22:23:27 | Автор: admin
В этой статье будет описано пошаговое создание фронтенда для прототипа SPA интернет магазина. Серверная часть которого написана на node.js с использованием express, шаблонизатора twig, базы данных NeDB, для обработки данных форм модуль formidable, клиентская часть написана на htmlix.

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

Исходные данные для данного урока можно скачать здесь.

Это уже готовый пример, мы просто удалим из файла /static/js/front.js все что там есть и вставим вместо этого:

var State = { };window.onload = function(){///создаем экземпляр  HTMLixvar HM = new HTMLixState(State);        console.log(HM);}

Итак мы полностью удалили фронтенд из данного примера, и у нас остался сервер приложения, давайте разберемся как он работает:

  • сперва нужно установить все модули которых сейчас нет, для этого введем в консоли: npm install
  • далее ввести в командной строке node app
  • затем перейти по адресу localhost:3000

Итак покликав по различным пунктам меню мы видим работающий сервер, без фронтенд части т.к. мы только что ее удалили.

Теперь заново поэтапно ее создадим, но для начала разберемся как работает сервер.

Перейдя по адресу localhost:3000/ нам отдается верхнее меню с тремя пунктами, основная страница с шестью карточками товара, и список категорий с левой стороны.

Чтобы отдать нам это все сервер посылает два запроса к базе данных, в одном он находит все категории, во втором первые шесть карточек товара из таблицы, затем присоединяет к каждой карточке товара два поля из совпадающей по id категории товара, затем отдает нам представление /views/index.twig передав в него массив с категориями categories и массив с карточками товара carts.

В самом представлении /views/index.twig мы загружаем из папки /twig_templates шапку сайта и верхнее меню:

{% include '/twig_templates/header.twig' %}

Далее создаем разметку для главной страницы и страницы категорий, в ней создаем список категорий из переданного в шаблонизатор массива categories:

<section class="categories" >   <ul class="nav flex-column" data-categories="array">        {% for category in categories %} <li  class="nav-item" data-category="container">   <a {% if activeCategory == category.idCategory %}class="nav-link active"       {% else %} class="nav-link"       {% endif %}      data-category-click="click" data-category-title="text" data-category-class="class" data-category-data="{{ category.idCategory }}" class="nav-link" href="http://personeltest.ru/aways/habr.com/category/{{ category.idCategory }}">{{ category.titleCategory }}</a></li>      {% endfor %}   </ul></section>

Пока что не обращаем внимания на записи в теге начинающиеся с data- все это относится к фронтенду и нам пока что не интересно.

Далее в цикле создаем все карточки товара из массива carts:

{% for cart in carts %}   <div data-cart="container" class="col-12 col-sm-6  col-lg-4"><div  class="card" style="margin-bottom: 10px;">   <img data-cart-src_img="src" src="http://personeltest.ru/aways/habr.com/static/upload/{{cart.image}}" class="card-img-top" alt="...">   <div class="card-body"><h5 data-cart-title="text" class="card-title">{{cart.title}}</h5><h6 data-cart-title_category="text" class="card-subtitle mb-2 text-muted">                        {{cart.titleCategory}}        </h6><div class="row justify-content-between">    <div class="col-6"><h5 data-cart-cost="text" class="card-title">{{cart.cost}}</h5>   </div>   <div class="col-6" style="padding-right: 0px;"><a data-cart-click="click"  data-cart-data="/cart/{{cart._id}}" href="http://personeltest.ru/aways/habr.com/cart/{{cart._id}}" class="btn btn-primary btn-sm">Смотреть</a>   </div>        </div>   </div></div>   </div> {% endfor %}

Затем в самом конце мы загружаем footer, в котором подключаем htmlix.js и frontend.js наш файл с фронтендом:

{% include '/twig_templates/footer.twig' %}

Аналогично для адреса /category/:idCategory мы также загружаем все категории, затем ищем карточки товара для данной категории а потом также передаем все это в представление /views/index.twig.

Также давайте сразу рассмотрим два адреса: /json и /category/:idCategory/json перейдя по этим адресам мы получим вместо представления массив с карточками товара в формате json, для первого адреса это первые шесть карточек, а для второго карточки соответствующей категории :idCategory.

Теперь разберем маршрут /cart/:idCart

Здесь мы загружаем все категории, затем одну карточку соответствующую :idCart затем также присоединяем ей два поля из таблицы категорий, далее отдаем представление /views/cart.twig передав в него карточку cart и массив с категориями categories.

Далее в представлении /twig_templates/header.twig. Подключаем header, затем создаем список категорий из массива categories.

Создаем разметку для карточки товара, в неё мы вставляем данные из объекта cart, а также подключаем один из двух вариантов шаблона из папки /twig_templates/: cart_variant_option.twig или cart_variant_radio.twig который мы выбираем при создании категории товара (доставка или количество).

<div class="col-md-5"   data-cart_single-variant_tmpl="render-variant">{% set templ = cart.variant_tmpl %}<!-- создали переменную set templ  на основе свойства объекта cart.variant_tmpl -->{% include  '/twig_templates/' ~ templ ~ '.twig' %} <!-- динамически вставили шаблон из папки '/twig_templates/' на основании переменной с именем шаблона  templ --></div>

В конце разметки подключаем footer.

Маршрут /cart/:idCart/json отдает пустой массив [], данный роут используется для заглушки.

Итак, мы достаточно знаем о сервере чтобы начать строить приложение пока что для трех роутов.

Давайте сначала разберемся с роутами "/" и "/category/:idCategory":

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

Здесь страницу можно разделить на два компонента это категории и карточки товара, давайте создадим два компонента в описании приложения это: categories и carts:

В html коде файла /views/index.twig они уже обозначены как data-carts=array компонент массив carts и data-cart=container контейнер cart компонента carts. data-categories=array компонент массив categories, data-category=container контейнер category компонента categories.

Создадим их описание в javascript:

var State = {categories: { //компонент категорииcontainer: "category", //контейнер компонента категорииprops: [ ],methods: {}},carts: { //компонент карточки товараcontainer: "cart", //контейнер компонента карточки товараprops: [ ],methods: {}}}

Итак мы создали два компонента categories и carts давайте добавим несколько свойств для компонента categories:

* data данные которые содержат idCategory,
* click обработчик кликов по категории,
* 'class' доступ к классу категории для изменения цвета текущей категории,
* title доступ к тексту внутри категории,

В html коде файла /views/index.twig они уже обозначены как data-category-data="{{ category.idCategory }}", data-category-click=click, data-category-class=class, data-category-title=text.

Добавим их в описание приложения:

categories: {container: "category", props: [ "data", "click", 'class', "title"],methods: {  click: function(){event.preventDefault();                               console.log(this);},}},

Итак мы создали четыре свойства для каждого контейнера категории ( category ), и для свойства click метод который пока что ничего не делает, только отменяет переход по ссылке и перезагрузку страницы. Давайте разберемся что нам нужно сделать при клике по категории:

1 загрузить массив с карточками товара для дальнейшего использования, чтобы построить компонент массив carts на основании полученных данных.
2 удалить класс .active у предыдущей и установить у новой категории.

Для первого пункта создадим метод load_carts в общих для всего приложения методах stateMethods и поместим загруженные карточки в переменную carts объекта stateProperties общих для всего приложения переменных;

Для второго пункта создадим пользовательское событие emiter-click-on-category и будем слушать его во всех контейнерах category чтобы добавить или удалить класс при его наступлении, вызывать событие будем в методе click контейнера category.

Итак напомню что свойства в массиве props контейнера, либо arrayProps массива можно обозначать двумя способами это, по имени свойства например click тогда нужно указывать его в html разметке как data-имя_контейнера-click=click.

И второй способ это массивом [ 'имя_свойства', тип_свойства, селектор для поиска свойства] тогда нам не требуется указывать данное свойство в html разметке, т. к. мы его ищем с помощью селектора, если оставить селектор пустым "" то свойство будет присвоено тегу самого контейнера, либо тегу самого массива для свойств arrayProps.

Изменим описание приложения:

var State = {  categories: {    container: "category",     //добавили слушателя события "emiter-click-on-category" определив свойство вторым способом    props: [ "data", "click", 'class', "title",  ['listner_click_on_category', "emiter-click-on-category", ""]],    methods: {click: function(){   event.preventDefault();   var categoryId = this.parent.props.data.getProp();    //вызываем событие "emiter-click-on-category"       // передав в него данные со свойства data контейнера по которому был клик    this.rootLink.eventProps["emiter-click-on-category"].setEventProp(categoryId);    //создаем url на основе данных со свойства data контейнера,    //один для истории - historyUrl понадобится нам в дальнейшем,           // второй чтобы сделать запрос для получения карточек товара на адрес /category/:idCategory/json               var historyUrl = "/category/"+categoryId;    var url = historyUrl+"/json";                      this.rootLink.stateMethods.load_carts(url, this); //вызываем метод load_carts и передаем в него url  и this чтобы не потерять контекст },listner_click_on_category: function(){ //в слушателе события клика по категории который мы вызываем в методе выше - удаляем класс "active" со всех контейнеров// затем устанавливаем его на контейнере данные свойства data которого совпадают        // с данными переданными в событие "emiter-click-on-category"this.parent.props.class.removeProp("active");if(this.parent.props.data.getProp() == this.emiter.prop){  this.parent.props.class.setProp("active");}},      }    },

    carts: {container: "cart",props: [ ],methods: {}     },     stateProperties:{ //объект для хранения общих переменных приложенияcarts: [],     },     stateMethods: {fetchCategoryCarts: function(url, callb){ // общий  метод для загрузки данных с какого либо адреса get запросм,        //принимает в параметрах адрес - url и функцию обратного вызова callb,      // в которую он передаст полученные данные    fetch(url).then((response) => {if(response.ok) {   return response.json();}            throw new Error('Network response was not ok');    }).then((json) => {callb(json);             }).catch((error) => { console.log(error);     });}, load_carts:  function(url, context ){ //метод вызывает fetchCategoryCarts передавая в него функцию обратного вызова // в которой подставляем данные с сервера в переменную .carts//в дальнейшем также будем вызывать здесь событие "emiter-load-carts", которое мы еще не создали, поэтому пока что закоментируем его   context.rootLink.stateMethods.fetchCategoryCarts(url, function(data){       context.rootLink.stateProperties.carts = data;       console.log(data)      //context.rootLink.eventProps["emiter-load-carts"].setEventProp(data);    });},     },     eventEmiters: {//создали объект со всеми пользовательскими событиями        ["emiter-click-on-category"] : { //добавили событие "emiter-click-on-category"prop: [],   },      }}

Итак теперь при клике по категории у нас меняется класс текущей категории с помощью события emiter-click-on-category, а также загружаются данные с сервера с помощью метода load_carts объекта stateMethods, которые мы сохраняем в свойстве carts объекта stateProperties и выводим пока что в консоль.

Далее давайте добавим свойства контейнера cart компонента carts:

* data данные которые содержат '/cart/:idCart' ( "/cart/{{cart._id}}" ),
* title_category название категории {{cart.titleCategory}},
* cost стоимость товара {{cart.cost}},
* 'title' доступ к тексту внутри карточки ( {{cart.title}} ),
* src_img адрес картинки карточки "/static/upload/{{cart.image}}",
* click обработчик события клика по карточке товара,

а также создадим дополнительное свойство компонента массива carts 'listener_load_carts' которое будет слушать событие emiter-load-carts загрузки карточек товара и обновлять массив с карточками при его наступлении, мы его будем вызывать в методе load_carts объекта stateMethods.

Теперь добавим свойство к нашему компоненту carts и все свойства к контейнеру cart компонента carts:

 carts: {    //добавили свойство listener_load_carts для массива carts     arrayProps: [ ['listener_load_carts', "emiter-load-carts", ""] ],     arrayMethods: {         listener_load_carts: function(){ //слушаем событие "emiter-load-carts"     this.parent.removeAll(); //переходим из свойства массива в сам массив и очищаем его              var carts = this.emiter.prop; //получаем массив из события "emiter-load-carts" которое мы вызовем в методе load_carts при клике по категории товара     for(var i=0; i<carts.length; i++){ //в цикле перебираем полученный массив с карточками товара var cart = carts[i];   var props = { //создаем объект со всеми изменяемыми свойствами для контейнера carttitle: cart.title,title_category: cart.titleCategory,cost: cart.cost,src_img: '/static/upload/'+cart.image,data: "/cart/"+cart._id   }this.parent.add(props);                 //добавляем новый контейнер в массив, передав в него начальные данные для свойств    }}    },    container: "cart",    //добавили все свойства для контейнера cartprops: ['title', "title_category",  "cost", "click", "src_img", "data"  ],methods: {     click: function(){ //метод для обработки кликов по карточке товара, //пока что просто выводит url карточки в консоль event.preventDefault(); var url = this.parent.props.data.getProp();  console.log(url);     }  } },

Также не забудем раскоментировать вызов события emiter-load-carts в методе load_carts и добавить новое событие в объект eventEmiters:

load_carts:  function(url, context ){    context.rootLink.stateMethods.fetchCategoryCarts(url, function(data){       context.rootLink.stateProperties.carts = data;        console.log(data)        context.rootLink.eventProps["emiter-load-carts"].setEventProp(data);         //раскомментировали вызов события "emiter-load-carts"         //теперь мы передаем в него массив carts полученный с сервера,               // в обработчике события их можно будет получить вызвав this.emiter.prop     });},   ///*********************************************   eventEmiters: {           // ------------------------------    ["emiter-load-carts"]: { //добавили новое событиеprop: "",     }      }

Ну вот теперь при клике по категории у нас загружаются данные с сервера и отображаются в компоненте carts, однако при клике по карточке товара пока что ничего не происходит, кроме вывода в консоль адреса просматриваемой карточки '/cart/:idCart'

Далее создадим компонент карточку товара cart_single, но прежде чем ее создать разберемся как работает htmlix:

Итак при загрузке страници с адреса "/" либо "/category/:idCategory", сервер присылает нам html код двух компонентов, точнее трех, еще menu, но его мы пока что не используем, мы используем html код для компонентов carts и categories, на основе этого кода htmlix создает шаблоны для компонентов, но шаблон для компонента cart_single нам не передается по данным адресам, т.к. его там нет. Как же нам его догрузить? Для этого, мы создаем компонент cart_single в описании приложения и помещаяем его в специальный объект fetchComponents а в настройках `stateSettings.templatePath` указываем адрес по которому загружать шаблоны для данного компонента, да и всех остальных, которые еще появятся. Таким образом приложение сначала создаст компоненты которые прислал сервер вместе с html, а затем после отправки второго запроса по адресу stateSettings.templatePath создаст остальные компоненты, находящиеся в объекте fetchComponents.

Так бы мы сделали если бы вход в приложения у нас всегда бы был с адресов, "/" либо "/category/:idCategory", а что если мы потом первую загрузку приложения произведем с адреса "/cart/:idCart"? Приложение выдаст ошибку, что не может найти шаблон для массива carts, так как теперь его не будет по этому адресу и нам нужно помещать теперь carts в объект fetchComponents a single_cart наоборот оттуда достать, т.к. он должен инициализироваться первым вместе с компонентом categories, что же делать?

Для решения этой задачи мы будем использовать HTMLixRouter(), он перед тем как создать экземпляр приложения, после загрузки страницы проверяет какой сейчас адрес url и сравнивает его с теми которые мы ему укажем, и затем сам помещает те для которых нет шаблона в первой загрузке, в объект fetchComponents, после чего инициализирует приложение, таким образом мы избежим создания, нескольких вариантов описания приложения для разных url и соответственно дублирования кода.

Итак у нас пока что есть три адреса "/" ,"/category/:idCategory" и "/cart/:idCart", три основных компонента carts, categories и cart_single. На первых двух адресах у нас первыми должны инициализироваться компоненты carts и categories, а на адресе "/cart/:idCart" categories и cart_single.

Создадим для них объект routes и поместим его отдельно от писания приложения State в переменную:

var routes = {    ["/"]: {        first: ["categories", 'carts'],  /// компоненты которые есть в html файле указываются в этом массиве, остальные будут загружены с шаблона, в fetch запросе асинхронноroutComponent: {                 router_carts: "carts", //компонент соответствующий данному роуту           }, templatePath: "/static/templates/index.html" // папка для загрузки шаблонов     },     ["/category/:idCategory"]: {  //знак `:` говорит что это параметр, и с ним сравненние не требуется, проверяется только его наличие в данной позиции url, еще есть знак `*` в конце слова например если у нас category1, category2, и т.д то ставим звездочку в конце category*. first: ["categories", 'carts', "menu", "home_page"],  routComponent: {         router_carts: "carts",},   templatePath: "/static/templates/index.html" },     ["/cart/:idCart"]: {   first: ["categories", 'cart_single'],   routComponent: {       router_carts: "cart_single",   },   templatePath: "/static/templates/index.html"      },}

Итак мы создали три маршрута для роутера, здесь router_carts это div элемент в котором будут отображаться carts на первых двух адресах, а на третьем cart_single, в html (в файлах '/views/index.twig' и '/views/cart.twig') он указан как data-router_carts=router.

Далее добавим в описание приложения cart_single вместе со следующими свойствами:
* variant_tmpl свойство с типом render-variant для отображения дополнительного варианта шаблона (cart_variant_option.twig или cart_variant_radio.twig в зависимости от категории) в html разметке data-cart_single-variant_tmpl-render-variant,
* 'title' Название карточки товара 'data-cart_single-title=text',
* title_category название категории,
* manufacture призводитель,
* cost стоимость,
* description описание,
* cost_btn стоимость в кнопке,
* src_img картинка data-cart_single-src_img=srс,
* data данные содержащие id категории для формирования url data-cart_single-data="{{ cart.category }}",
* click_category событие клика по категории в карточке товара,
* listner_click_on_cart слушатель пользовательского события emiter-click-on-cart,

cart_single: {   container: "cart_single",   //пока что закомментируем "variant_tmpl" т.к. мы еще не создали компоненты с вариантами шаблонов для карточки   props: [/*"variant_tmpl",*/'title', "title_category", "manufacture", "cost", "description", "cost_btn", "src_img", ["listner_click_on_cart", "emiter-click-on-cart", ""], "click_category", "data"],   methods: {listner_click_on_cart: function(){var index = this.emiter.getEventProp();                  //получаем индекс контейнера карточки по которой кликнули в компоненте cartsvar cart = this.rootLink.stateProperties.carts[index];                //выбираем из загруженных раннее в массиве карточек нужную нам по индексу контейнера var props = {   title: cart.title,   title_category: cart.titleCategory,   cost: cart.cost,   src_img: '/static/upload/'+cart.image,   data: cart.category,   description:  cart.description,   cost_btn: cart.cost,   manufacture: cart.manufacture,  // variant_tmpl: cart.variant_tmpl, пока что закоментируем установку вариантов шаблона, т.к. мы еще не создали  для них компоненты. }this.parent.setAllProps(props);         //устанавливаем новые значения сразу для всех свойств, с помощью метода setAllProps(props);},click_category: function(){   event.preventDefault();            //данный метод создадим чуть позже, пока что просто выводим id категории в консоль           console.log(this.parent.props.data) }      },},

Далее добавим в метод click контейнера 'cart' компонента carts дополнительный код

click: function(){                    event.preventDefault();   var url = this.parent.props.data.getProp(); //в свойстве data контейнера cart у нас url крточки товара.     //добавили вызов события  "emiter-click-on-cart"  которое мы слушаем в компоненте single_cart и обновляем данные всех свойств    //передаем в него индекс контейнера по которому кликнули    this.rootLink.eventProps["emiter-click-on-cart"].setEventProp(this.parent.index);      //вызываем метод setRout передав в него новый url,   // чтобы роутер поменял компонент carts на cart_single в div элементе data-router_carts="router" и обновил историю в броузере     this.rootLink.router.setRout(url);}

Далее добавляем новое событие emiter-click-on-cart в объект eventEmiters

     eventEmiters: {   ["emiter-click-on-cart"]: {prop: "",},

А также в конец метода click контейнера catgory компонента categories:

click: function() {        //метод setRout принимает url и сравнивает его с картой которую мы создали в объекте routes    //и на соответствующем адресе меняет компонент в div теге  `data-router_carts="router"` отображая carts или single_cart     /* ............ конец метода ............*/     this.rootLink.router.setRout(historyUrl);    }

И не забываем заменить способ загрузки приложения, теперь мы передаем наши роуты routes и описание приложения State в функцию HTMLixRouter().

window.onload = function(){///создаем экземпляр  HTMLixvar HM = HTMLixRouter(State, routes);        var url = window.location.pathname;    if(window.location.pathname == "/"){url = url+"json";    }else{ url = url + "/json";    }                    //отправляем запрос чтобы загрузить массив со всеми карточками товара при первой загрузке приложения  HM.stateMethods.fetchCategoryCarts(url, function(arr){  HM.stateProperties.carts = arr; });  console.log(HM);}

Ну вот теперь наше приложение работает для трех адресов url, за исключением смены варианта шаблона для single_cart и клика по категории из single_cart. Давайте покликаем посмотрим что все в порядке.

Теперь создадим два новых компонента в описании приложения для смены варианта шаблона в компоненте single cart:

cart_variant_option: {container: "cart_variant_option", // views/twig_templates/cart_variant_option.twig =>  data-cart_variant_option="container"props: ["click", "select"],  //data-cart_variant_option-select="select" , data-cart_variant_option-click="click"methods: { click: function(){console.log(this.parent.props.select.getProp());}}},cart_variant_radio: {selector: "div:last-of-type",container: "cart_variant_radio_cont",props: ["click", "radio"],methods: { click: function(){console.log(this.parent.index+" --- "+this.parent.props.radio.getProp());}}},

После чего раскомментируем свойство variant_tmpl компонента контейнера cart_single

cart_single: {   container: "cart_single",   props: ["variant_tmpl" /*...........................*/],

И в методе listner_click_on_cart:

var props = {   title: cart.title,   title_category: cart.titleCategory,   cost: cart.cost,   src_img: '/static/upload/'+cart.image,   data: cart.category,   description:  cart.description,   cost_btn: cart.cost,   manufacture: cart.manufacture,   variant_tmpl: cart.variant_tmpl, //раскомментировали }

Теперь при клике на карточку товара, в разных категориях товара у нас будет разный шаблон, а в свойстве variant_tmpl один из двух компонентов cart_variant_radio ноутбуки и телевизоры, cart_variant_option фотоаппараты.

Далее давайте добавим код в метод click_category компонента контейнера cart_single чтобы при клике по категории из нутри карточки товара у нас также менялась активная категория и изменялся отображаемый в роутере компонент, просто скопируем код из метода click контейнера category компонента categories.

click_category: function(){   event.preventDefault();    this.rootLink.eventProps["emiter-click-on-category"].setEventProp(this.parent.props.data.getProp());               var historyUrl = "/category/"+this.parent.props.data.getProp();    var url = historyUrl+"/json";                      this.rootLink.stateMethods.load_carts(url, this);   this.rootLink.router.setRout(historyUrl);},

Итак метод работает но плохо то что мы продублировали код, давайте перенесем весь код в метод click_on_category в объект с общими методами для всего приложения stateMethods, и предадим в него context параметром, а вторым url который хотим сменить:

click_on_category: function(context, event){event.preventDefault();var historyUrl = "/category/"+context.parent.props.data.getProp();var url = historyUrl+"/json";context.rootLink.eventProps["emiter-click-on-category"].setEventProp(context.parent.props.data.getProp());context.rootLink.router.setRout(historyUrl);console.log(url);context.rootLink.stateMethods.load_carts(url, context);},

А в методе click_category компонента контейнера cart_single и методе click контейнера category компонента categories вызовем общий метод:

/* */ : function(){     this.rootLink.stateMethods.click_on_category(this, event);} 

Итак мы создали первую часть прототипа интернет магазина для трех адресов url, полный скрипт приложения до этой точки приведен ниже, на этом пока что все.

var State = {  categories: {    container: "category",     props: [ "data", "click", 'class', "title",  ['listner_click_on_category', "emiter-click-on-category", ""]],    methods: {click: function(){this.rootLink.stateMethods.click_on_category(this, event);},listner_click_on_category: function(){ this.parent.props.class.removeProp("active");if(this.parent.props.data.getProp() == this.emiter.prop){  this.parent.props.class.setProp("active");}},      }    },    carts: {arrayProps: [ ['listener_load_carts', "emiter-load-carts", ""] ],arrayMethods: {   listener_load_carts: function(){ this.parent.removeAll();for(var i=0; i<this.emiter.prop.length; i++){//в цикле перебераем полученый массив с карточками товараvar prop = this.emiter.prop[i];var props = { title: prop.title,title_category: prop.titleCategory,cost: prop.cost,src_img: '/static/upload/'+prop.image,data: "/cart/"+prop._id}this.parent.add(props); //добавляем новый контейнер передав в него начальные данные для свойств}}},container: "cart",//добавили все свойства для контейнера cartprops: ['title', "title_category",  "cost", "click", "src_img", "data"  ],methods: {click: function(){ //метод для обработки кликов по карточке товара,            event.preventDefault();var url = this.parent.props.data.getProp();this.rootLink.router.setRout(url);    this.rootLink.eventProps["emiter-click-on-cart"].setEventProp(this.parent.index);}    }     },cart_single: {   container: "cart_single",   props: ["variant_tmpl",'title', "title_category", "manufacture", "cost", "description", "cost_btn", "src_img", ["listner_click_on_cart", "emiter-click-on-cart", ""], "click_category", "data"],   methods: {listner_click_on_cart: function(){var index = this.emiter.getEventProp();                  //получаем индекс контейнера карточки по которой кликнули в компоненте cartsvar cart = this.rootLink.stateProperties.carts[index];                //выбираем из загруженных раннее в массиве карточек нужную нам по индексу контейнера var props = {   title: cart.title,   title_category: cart.titleCategory,   cost: cart.cost,   src_img: '/static/upload/'+cart.image,   data: cart.category,   description:  cart.description,   cost_btn: cart.cost,   manufacture: cart.manufacture,  variant_tmpl: cart.variant_tmpl,  }this.parent.setAllProps(props);         //устанавливаем новые значения сразу для всех свойств, с помощью метода setAllProps(props);},   click_category: function(){this.rootLink.stateMethods.click_on_category(this, event);    },},},cart_variant_option: {container: "cart_variant_option",props: ["click", "select"],  methods: { click: function(){console.log(this.parent.props.select.getProp());}}},cart_variant_radio: {selector: "div:last-of-type",container: "cart_variant_radio_cont",props: ["click", "radio"],methods: { click: function(){console.log(this.parent.index+" --- "+this.parent.props.radio.getProp());}}},     stateProperties:{ //объект для хранения общих переменных приложенияcarts: [],     },     stateMethods: {   click_on_category: function(context, event){ //общий метод для кликов по категории для компонентов categories и cart_singleevent.preventDefault();var historyUrl = "/category/"+context.parent.props.data.getProp();var url = historyUrl+"/json";context.rootLink.eventProps["emiter-click-on-category"].setEventProp(context.parent.props.data.getProp());context.rootLink.router.setRout(historyUrl);console.log(url);context.rootLink.stateMethods.load_carts(url, context);},fetchCategoryCarts: function(url, callb){ // общий  метод для загрузки данных с какого либо адреса get запросм,        //принимает в параметрах адрес - url и функцию обратного вызова callb в которую он передаст полученые данные    fetch(url).then((response) => {if(response.ok) {   return response.json();}            throw new Error('Network response was not ok');    }).then((json) => {callb(json);             }).catch((error) => { console.log(error);     });}, load_carts:  function(url, context ){ //метод вызывает fetchCategoryCarts передавая в него функцию обратного вызова // в которой подставляем данные с сервера в переменную .carts//а также вызывает событие "emiter-load-carts" и передает в него массив с карточками   context.rootLink.stateMethods.fetchCategoryCarts(url, function(data){       context.rootLink.stateProperties.carts = data;      context.rootLink.eventProps["emiter-load-carts"].setEventProp(data);    });},     },     eventEmiters: {//создали объект со всеми пользовательскими событиями   ["emiter-click-on-cart"]: {prop: "",},         ["emiter-click-on-category"] : { prop: [],   },   ["emiter-load-carts"]: {prop: "",}      }}var routes = {    ["/"]: {        first: ["categories", 'carts'],  /// компоненты которые есть в html файле указываются в этом массиве, остальные будут загружены с шаблона, в fetch запросе асинхронноroutComponent: {                 router_carts: "carts", //компонент соответствующий данному роуту           }, templatePath: "/static/templates/index.html" // папка для загрузки шаблонов     },     ["/category/:idCategory"]: {  //знак `:` говорит что это параметр, и с ним сравненние не требуется, проверяется только его наличие, еще есть знак `*` в конце слова например если у нас category1, category2, и т.д то ставим звездочку в конце category*. first: ["categories", 'carts', "menu", "home_page"],  routComponent: {         router_carts: "carts",},   templatePath: "/static/templates/index.html" },     ["/cart/:idCart"]: {   first: ["categories", 'cart_single'],   routComponent: {       router_carts: "cart_single",   },   templatePath: "/static/templates/index.html"      },}window.onload = function(){///создаем экземпляр  HTMLixvar HM = HTMLixRouter(State, routes);var url = window.location.pathname;if(window.location.pathname == "/"){url = url+"json";}else{url = url + "/json";}HM.stateMethods.fetchCategoryCarts(url, function(arr){  HM.stateProperties.carts = arr; }); //загружаем массив с карточками после инициализации приложения, для дальнейщего использования.console.log(HM);}
Подробнее..

Работа с пользовательскими событиями в htmlix.js

21.06.2020 10:10:12 | Автор: admin
В данной статье будут рассмотрены базовые принципы работы с пользовательскими событиями в Htmlix и создано небольшое приложение из четырех компонентов: формы ввода имени пользователя, приветствия, кнопки выхода из приложения, и массива с пользователями приложения.

Уже готовый пример можно покликать здесь

Почитать api htmlix можно здесь

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

Давайте создадим четыре компонента: форму входа form, приветствие greeting, кнопку выхода logout и массив со всеми посетителями users_array, и посмотрим как бы мы создавали между ними коммуникацию если бы не пользовались пользовательскими событиями.


Создадим html разметку всех компонентов:

<!-- компонент - контейнер форма с двумя свойствами input c типом свойства "inputvalue" и  click с типом "click" --><form data-form="container" class="card col-12">   <div class="form-group">       <label for="">Введите имя</label>      <textarea data-form-input="inputvalue" class="form-control" rows="1"></textarea>    </div>    <button data-form-click="click" type="submit">Submit</button></form><!-- компонент - контейнер приветствие и свойство user_name с типом - "text" --><div data-greeting="container" class="col-6 card">    <p>Привет: <span  data-greeting-user_name="text">guest</span></p></div><!-- компонент - контейнер кнопка выхода и свойство user_name с типом - "text" --><div data-logout="container" class="col-6 card">      <a href="#"> Выйти из профиля: ( <span  data-logout-user_name="text"></span>  )      </a></div><!-- компонент - массив пользователей, изначально с двумя контейнерами, в каждом контейнере свойство user_name - "text"  --> <div class="container-fluid" style="border: 1px solid red; margin-top: 20px;">     <p> все пользователи:</p>       <div data-users_array="array" class="row">         <div data-user="container" class="col-4 card">     <p>пользователь -                     <span data-user-user_name="text">user_name_1</span>             </p></div>         <div data-user="container" class="col-4 card">     <p>пользователь -                     <span data-user-user_name="text">user_name_1</span>             </p></div>     </div></div>


Теперь перенесем их в javascript:

var StateMap = {form: {//форма входаcontainer: "form",props: ["input", "click"],methods: {click: function(){event.preventDefault(); //отменяем перезагрузку страницы                                 //получаем данные свойства inputvar text = this.parent.props.input.getProp(); console.log(text);}},},greeting: {//приветствиеcontainer: "greeting",props: [ "user_name", ], methods: {        }},logout: { //кнопка выходаcontainer: "logout",props: [ "user_name", ], methods: {},},users_array: { //массив с пользователями container: "user",props: [ "user_name", ],methods: {  },        },}window.onload = function(){//создаем экземпляр приложения htmlixvar HM = new HTMLixState(StateMap);console.log(HM);}


В примере выше мы записали данные свойства input из компонента формы в переменную text. Теперь нам нужно отобразить их в свойствах компонентов greeting и logout, а также создать новый контейнер в массиве users_array. Для понимания того зачем нужны пользовательские события давайте сначала попробуем сделать это различными способами без их использования.

1. способ в самом методе click формы напрямую перейти к каждому компоненту и установить свойство text:

        event.preventDefault();        var text = this.parent.props.input.getProp();                //установили значения свойствам                this.rootLink.state["greeting"].props.user_name.setProp(text);                this.rootLink.state["logout"].props.user_name.setProp(text);                                    ///создали новый контейнер                 this.rootLink.state["users_array"].add({user_name: text});


Недостаток данного подхода очевиден, что если потом мы где-нибудь еще будем получать данные для этого свойства? Например на основе localstorage они будут загружаться автоматически.

2. способ это создать метод в объекте stateMethods и вызывать его при изменении данных:

ststeMethods: {         entryUser: function(text){                this.state["greeting"].props.user_name.setProp(text);                this.state["logout"].props.user_name.setProp(text);                 this.state["users_array"].add({user_name: text});       }}// после загрузки странициwindow.onload = function(){             var name = window.localStorage.getItem('user_name');             if(name)HM.stateMethods.entryUser.call.(HM, name); ///вызываем метод передав ему контекст= HM}//в формеclick: function(){                  event.preventDefault();  var text = this.parent.props.input.getProp();                  this.rootLink.stateMethods.entryUser.call.(this.rootLink, text); }


В данном случае нам удалось избежать дублирования кода, однако проблема в том что методы для работы со свойствами компонента находятся не внутри него а во внешних функциях. Что если перед тем как установить свойство его нужно как-то отформатировать, по разному для каждого компонента, или нам нужно будет установить не одно свойство, а несколько, поэтому лучше когда компоненты сами работают со своими свойствами.

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

greeting: {container: "greeting",                //добавили вспомогательный метод для приветствия пользователяprops: [ "user_name", ["greet_user", "aux"] ], methods: {                                            greet_user: function(name){                                this.props.user_name.setProp(name+" !!!");                     }        }},logout: { container: "logout",props: [ "user_name", ["set_name", "aux"]], //добавили вспомогательный метод для работы со свойством компонентаmethods: {                     set_name: function(name){                                this.props.user_name.setProp(name);                     }                          },},users_array: { arrayProps: [["entry_user", "aux"]],  ///добавляем контейнер с новым пользователем из массива                arrayMethods: {                       entry_user: function(name){                              this.add({user_name: name})                       }               }container: "user",props: [ "user_name", ],methods: {  },        },    stateMethods: {   //изменили общий метод                   entryUser: function(text){                this.state["greeting"].methods.greet_user(text);                this.state["logout"].methods.set_name(text);                this.state["users_array"].entry_user(text);        }   }   //далее также вызываем данный метод entryUser из формы и при загрузке страницы


Теперь компоненты инкапсулированы, однако мы производим доступ к компоненту по его имени. Что если мы потом захотим изменить имя компонента или метода, или вообще удалить компонент, или у нас будет много таких компонентов которые слушают одно и тоже свойство. Или у нас будет много таких свойств, соответственно прийдется создавать много таких методов.

Чтобы облегчить данную задачу можно воспользоваться пользовательскими событиями. Компонент подписывается на событие, и при его наступлении делает что-то со своими свойствами.

Давайте отредактируем код, теперь с использованием пользовательских событий:

var StateMap = {   eventEmiters: {             //создали эмитер события - входа пользователя             ["emiter-entry-user"]: {prop: ""},    },   form: {container: "form",props: ["input", "click"],methods: {click: function(){    event.preventDefault();    var text = this.parent.props.input.getProp();     //вызвали событие в форме и передали в него данные    this.rootLink.eventProps["emiter-entry-user"].setEventProp(text);     window.localStorage.setItem('user_name', text);}},},greeting: {container: "greeting",props: [ "user_name", ['listen_entry_user', "emiter-entry-user", "" ] ], //добавили слушатель события "emiter-entry-user"methods: {listen_entry_user: function(){   //получили данные из события и обновили свойство   this.parent.props.user_name.setProp( this.emiter.getEventProp() );},},},logout: { container: "logout",props: [ "user_name", ["listen_entry_user", "emiter-entry-user", "" ]], //свойство слушатель события "emiter-entry-user"methods: {listen_entry_user: function(){     //получили данные из события и вызвали свойство      this.parent.props.user_name.setProp( this.emiter.getEventProp() );},},},    //здесь слушатель события добавляется в свойство массива, т.к. если добавить его в контейнер оно будет вызвано для каждого контейнера users_array: { arrayProps: [ ['listen_entry_user', "emiter-entry-user", ""] ], //свойство слушатель события "emiter-entry-user"   arrayMethods: {listen_entry_user: function(){//получили данные из события и создали новый контейнерthis.parent.add( {user_name: this.emiter.getEventProp()} );},},container: "user",props: [ "user_name",  ['listen_exit_user', "emiter-exit-user", ""]  ],methods: {}},        },}window.onload = function(){var HM = new HTMLixState(StateMap);var name = window.localStorage.getItem('user_name');if(name != null)HM.eventProps["emiter-entry-user"].setEventProp(name); ///вызвали событие "emiter-entry-user" при загрузке сайта и передали в него данные}


Таким образом мы избавились от промежуточного метода entryUser и просто вызываем событие передав в него новые данные в форме и после загрузки страницы. Теперь если нам нужно изменить что-либо в компоненте, или вообще удалить его нам не потребуется поправлять код в каждой функции, которых может быть сколько угодно. А чтобы отписаться от какого либо события можно просто удалить слушателя в компоненте, или временно отключить его с помощью метода disableEvent() в свойстве подписчике.

Полный код данного примера вместе со вторым событием emiter-exit-user можно посмотреть здесь
Подробнее..

Объектно-ориентированная альтернатива jquery

18.07.2020 08:05:49 | Автор: admin
Много кто использует jquery в своих проектах, в связи с простотой и легкостью, с которой его можно подключить на страницу, несмотря на то что существует достаточное количество объектно ориентированных фреймворков с компонентным подходом. Однако почти все они создают html разметку из javascript кода, что не всегда удобно, для совместного их использования с другими библиотеками, также создание разметки на js занимает какое-то время на стороне клиента, либо приходится делать серверный рендеринг на node.js что исключает их использование с другими языками например php.

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

Было-бы здорово иметь один инструмент который может справиться со всеми требуемыми задачами, а именно:

  1. Встраиваться в уже отданную сервером страницу, без необходимости серверного рендеринга на node.js и возможность использования его с любыми другими библиотеками
  2. Иметь объектно-ориентированный, компонентный подход построения приложения, четкую и понятную структуру, возможность наследования свойств.
  3. Легкость в изучении, небольшой размер, производительность, возможность применять его как в маленьких так и больших проектах, а также для создания виджетов, слайдеров и т.д.


Как вы наверное уже догадались, здесь я как раз и хочу его представить htmlix.js


Фреймворк встраивается в уже отданную сервером страницу с помощью data- свойств.
Таким образом нам не нужно создавать шаблоны на каком либо другом языке в js коде, мы просто используем уже готовый html.

Далее создадим небольшое приложение, чтобы в сжатом виде познакомиться со всеми основными особенностями фреймворка.

Уже готовый, похожий пример можно покликать здесь.

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

Давайте создадим четыре компонента: форму входа form, приветствие greeting, кнопку выхода logout и массив со всеми посетителями users_array.

Создадим html разметку всех компонентов:

<!--  компонент - контейнер форма с двумя свойствами input c типом свойства "inputvalue" и  click с типом "click" --><form data-form="container" class="card col-12">   <div class="form-group">       <label for="">Введите имя</label>      <textarea data-form-input="inputvalue" class="form-control" rows="1"></textarea>    </div>    <button data-form-click="click" type="submit">Submit</button></form><!-- компонент - контейнер приветствие и свойство user_name с типом - "text" --><div data-greeting="container" class="col-6 card">                <p>Привет: <span  data-greeting-user_name="text">guest</span></p></div><!-- компонент - контейнер кнопка выхода и свойство user_name с типом - "text" --><div data-logout="container" class="col-6 card">              <a href="#"> Выйти из профиля: ( <span  data-logout-user_name="text"></span>  )      </a>                </div><!-- компонент - массив пользователей, изначально с двумя контейнерами, в каждом контейнере свойство user_name - "text", и свойством массива  message с типом "text" для отображения сообщений --> <div data-users_array="array">     <p  data-users_array-message="text" > </p> <div  class="row">            <div data-user="container" class="col-4 card"><p>пользователь - <span data-user-user_name="text">user_name_1</span>                 </p>     </div>             <div data-user="container" class="col-4 card"> <p>пользователь - <span data-user-user_name="text">user_name_2</span>                  </p>      </div>        </div></div>


В коде выше мы создали четыре компонента: три из них с типом container и один с типом array массив со всеми посетителями. Контейнер это компонент, в котором сгрупированы, какие либо html свойства, например текст, атрибут, слушатель события и т.д. Все свойства имеют определенный тип. Массив это компонент для работы с однотипными контейнерами, у массива так-же как и у контейнера могут быть различные свойства.

В примере выше мы указали свойства в html разметке с помощью data- атрибутов, также свойства можно указать с помощью селекторов из javascript кода, чтобы не писать их в разметке.

Теперь перенесем их в javascript:

var StateMap = {    form: {//форма входа        container: "form",        props: ["input", "click"],        methods: {            click: function(){                event.preventDefault(); //отменяем перезагрузку страницы                                 //this - указывает на свойство, в обработчике события которого мы находимся, parent - доступ к контейнеру из свойства                //получаем данные свойства input                 var text = this.parent.props.input.getProp();                 console.log(text);                                      }                   },          },      greeting: {//приветствие        container: "greeting",        props: [ "user_name", ],         methods: {            }    },    logout: { //кнопка выхода        container: "logout",        props: [ "user_name", ],         methods: {        },      },    users_array: { //массив с пользователями         selector: "div.row", //уточняющий селекторarrayProps: [ "message"], //свойство массива         container: "user",  //контейнер массива        props: [ "user_name", ], //свойство контейнера        methods: {                },              },}window.onload = function(){//создаем экземпляр приложения htmlix    var HM = new HTMLixState(StateMap);    console.log(HM);    }


В примере выше мы перенесли компоненты в javascript код. В компоненте form свойстве click мы записываем данные введенные пользователем в переменную text и выводим их пока-что в консоль. Далее эти данные мы будем использовать во всех оставшихся компонентах. Для того чтобы компоненты узнали о том что данные обновились мы будем использовать пользовательские события.

Добавим событие emiter-set-name:

eventEmiters: {  //добавляем объект со всеми пользовательскими событиями в описание приложения   ["emiter-set-name"]: {prop: ""}, //событие с начальными данными},///вызываем событие в свойстве click компонента form и передаем в него полученные данныеclick: function(){        event.preventDefault();         var text = this.parent.props.input.getProp();         this.rootLink.eventProps["emiter-set-name"].setEventProp(text);          //сохраняем имя пользователя в localStorage                          window.localStorage.setItem('user_name', name); } //слушаем событие во всех остальных компонентах и обновляем их свойстваgreeting: {     container: "greeting",     props: [ "user_name", ['listen_set_name', "emiter-set-name", "" ],], //создали свойство слушатель события в компоненте      methods: {          listen_set_name: function(){//получили данные из события и обновили свойство user_namethis.parent.props.user_name.setProp( this.emiter.getEventProp() );  }       },},logout: {    container: "logout",   props: [ "user_name",  ['listen_set_name', "emiter-set-name", "" ] ],    methods: { listen_set_name: function(){//слушаем событие и обновляем свойство user_name    this.parent.props.user_name.setProp( this.emiter.getEventProp() );},    },},users_array: {  selector: "div.row",arrayProps: [    "message",  ['listen_set_name', "emiter-set-name", ""],], arrayMethods: {   listen_set_name: function(){//слушаем событие в свойстве массива и создаем новый контейнер с полученными данными            this.parent.add( {user_name: this.emiter.getEventProp()} );                                       //выводим сообщение о новом посетителе                    this.parent.props.message.setProp("новый посетитель - "+this.emiter.getEventProp());     },container: "user",props: [ "user_name", ],methods: {},        },window.onload = function(){var HM = new HTMLixState(StateMap);                //проверяем есть ли переменная  user_name в  localStorage и при ее наличии вызываем событие var name = window.localStorage.getItem('user_name');if(name != null)HM.eventProps["emiter-set-name"].setEventProp(name); console.log(HM);}


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

Теперь создадим событие emiter-exit-user для выхода пользователя:

eventEmiters: {/*      ...        */["emiter-exit-user"]: {prop: ""}, }          //вызываем событие в компоненте logout и передаем в него имя пользователяlogout: {     container: "logout",    props: [ /* ...  */,  ["exit", "click", "a:first-of-type"],], //создали новое свойство - слушатель события "click" и указали его вторым способом с помощью массива      methods: {           /* ... */   exit: function(){      //получаем имя пользователя     var user_name = this.parent.props.user_name;     //вызываем событие     this.rootLink.eventProps["emiter-exit-user"].setEventProp( user_name.getProp() );                     //удаляем пользователя из localStorage     window.localStorage.removeItem('user_name');                              //очищаем свойство компонента      user_name.setProp("");    }},},users_array: {  selector: "div.row",arrayProps: [ /* .. */,   ['listen_exit_user', "emiter-exit-user", ""], ], //добавили свойство - слушатель события  "emiter-exit-user"arrayMethods: {/* ... */listen_exit_user: function(){//data - доступ ко всем контейнерам из массиваthis.parent.data.forEach(container=>{if(container.props.user_name.getProp() == this.emiter.getEventProp()){         container.remove(true); //удалили контейнер }});                 //добавили сообщение                 this.parent.props.message.setProp( this.emiter.getEventProp()+" - покинул сайт");},},container: "user",props: [ "user_name", ],methods: {},        },greeting: {container: "greeting",props: [/* ... */, ['listen_exit_user', "emiter-exit-user", ""] ], methods: {listen_exit_user: function(){        //удалили имя пользователя из свойстваthis.parent.props.user_name.setProp("");}},},


Это был краткий обзор основных принципов построения приложения на htmlix, полный код данного примера, вместе с использованием вспомогательных методов можно посмотреть здесь
Подробнее..

Создание модуля на фреймворке Htmlix

23.09.2020 12:10:47 | Автор: admin
В данной статье будет описаны базовые принципы создания модулей на javascript фреймворке htmlix.

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

Модули в htmlix полностью автономны и не требуют добавления в основной (core) участок кода. Поэтому их можно создать на отдельном js скрипте и подключить когда это потребуется или не подключать вовсе. Информацию об изменении состояния приложения и новые данные они получают на основе пользовательских событий см. Работа с пользовательскими событиями, поэтому их легко добавить или удалить не меняя основной код приложения. Можно также выключить прослушивание данных событий из самого модуля, а потом снова включить.

Далее будет рассмотрен модуль addDrawCirclePane приложения Collage_n который рисует круги на канвас с помощью кликов мыши. Перед рисованием модуль принимает два параметра цвет и диаметр круга с помощью свойств с типом inputvalue. Далее после нажатия кнопки рисовать вызывает emiter событие emiter-operation-with со значением draw-circle чтобы включить активное состояние модуля, и выключить другие модули приложения.

Модуль создается также как и обычный компонент:

(function(){//разметка модуля// создаем контейнер для модуля data-draw_circle_panel="container"//все используемые в js поля и кнопки обозначены именами (для удобства)// name="draw_circle_btn", name="draw_sircle_radius", name="draw_sircle_color"  var html = `   <div data-draw_circle_panel="container"  class="form-group" name="draw_circle_panel"><label for="exampleFormControlInput1" style="font-size: 15px;">                       Рисовать окружность        </label><div class="form-row"><div class="form-group col-md-4">                     <button type="button" name="draw_circle_btn" class="btn btn-success btn-sm">                            Рисовать                      </button></div><div class="form-group col-md-4">   <input name="draw_sircle_radius" type="text" class="form-control form-control-sm"></div><div class="form-group col-md-4">     <input name="draw_sircle_color" type="text" class="form-control form-control-sm"></div></div> </div>`  ;  //динамическое добавление разметки модуля в общую разметку приложения.  var div = document.createElement("div");  div.innerHTML = html;  div = div.querySelector("div");  var parent = document.querySelector("[data-main_form]");  var insert_before = document.querySelector("[name='common_btns_class']")  var insertedElement = parent.insertBefore(div, insert_before);    //js код модуля  var draw_circle_panel = {    container: "draw_circle_panel", //контейнер модуля  props: [               ///свойства модуля["draw_circle_btn", "click", "[name='draw_circle_btn']"], ["draw_sircle_radius", "inputvalue", "[name='draw_sircle_radius']"],["draw_sircle_color", "inputvalue", "[name='draw_sircle_color']"], ///два свойства-события основного core приложения: клики по канвас и событие смены операции["canvas_click", "emiter-mousedown-canvas", ""], ["operation_with", "emiter-operation-with", ""],  ],  methods: { //отключает слушателей canvas событий ( mousedown) если модуль находится в пассивном состоянии  operation_with: function(){    if(this.emiter.prop != "draw-circle"){     this.parent.props.canvas_click.disableEvent();    }else{    this.parent.props.canvas_click.enableEvent();    }    },//при нажатии на кнопку рисовать - вызывает событие "emiter-operation-with" и устанавливает свойство prop = "draw-circle" чтобы другие модули отключили прослушивание событий и скрыли ненужные кнопки.  draw_circle_btn: function(){  this.$$("emiter-operation-with").set("draw-circle");       },//слушает событие приложения  "emiter-mousedown-canvas" и в активном состоянии рисует круги при кликах мышью. canvas_click: function(){if(this.$$("emiter-operation-with").prop == "draw-circle"){//данные из свойств модуля  var props = this.parent.props;  var radius = props.draw_sircle_radius.getProp();  var color = props.draw_sircle_color.getProp();                       var point = this.emiter.prop;//данные из события основного (core) приложения с координатами точки на канвас             saveStep(saveImg, this.$props().commonProps.area_1);  //обычная функции из глобальной области для сохранения шагов, редактирования ctx.save();            ctx.putImageData(saveImg, 0, 0);ctx.beginPath();ctx.arc(point[0], point[1], radius, 0, 2*Math.PI, false);ctx.fillStyle =  color;ctx.fill();ctx.lineWidth = 1;ctx.strokeStyle =  color;ctx.stroke();                               //переменная из глобальной области для сохранения картинки после рисования saveImg = ctx.getImageData(0,0, srcWidth, srcHeight);ctx.restore();}}    }    }//добавляем описание модуля в общее описание приложения  HM.description.draw_circle_panel  = draw_circle_panel;//создаем контейнер модуля  HM.containerInit(div , HM.description, "draw_circle_panel");  HM.eventProps["emiter-operation-with"].emit(); //вызываем чтобы отключить слушателей canvas событий при старте модуля})()

В примере выше мы подключили контейнер с помощью функции: HM.containerInit(htmlLink, HM.description, module_name);
где HM ссылка на экземпляр приложения.

Для подключения массива нужно использовать функцию HM.arrayInit(htmlLink, HM.description, module_name);

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

Это был краткий обзор основных принципов создания и подключения htmlix модулей.
Подробнее..

Перевод Какой будет новая версия Vuex?

27.12.2020 20:07:49 | Автор: admin
Vuex стейт менеджер для Vue приложений. Его следующая версия Vuex 4, которая практически готова к официальному релизу. Она добавит поддержку Vue 3, но не принесет никакой новой функциональности.

Несмотря на то, что Vuex считается отличным решением и многие разработчики выбирают его как основную библиотеку для управления состоянием, они надеются получить больше возможностей в будущих релизах. Поэтому, пока Vuex 4 только готовится к выходу, один из его разработчиков, Kia King Ishii (входит в состав core-команды) уже делится планами для следующей, 5 версии. Стоит заметить, что это только планы и некоторые вещи могут измениться, тем не менее основное направление уже похоже выбрано. О нем и пойдет речь.

С появлением Vue 3 и Сomposition API, разработчики стали создавать простые альтернативы. Например, в статье Вероятно вам не нужен Vuex демонстрируется простой, гибкий и надежный способ для создания сторов на основе Composition API совместно с provide/inject. Можно предположить, что этот и некоторые другие альтернативы вполне подойдут для небольших приложение, но как часто бывает, они имеют свои недостатки: документация, сообщество, соглашение в именовании, интеграция, инструменты разработчика.



Последний пункт имеет большое значение. Сейчас у Vue есть отличное расширение для браузера, помогающее в разработке и отладке. Отказ от него может стать большой потерей, особенно при создании больших приложений. Но к счастью, с Vuex 5 этого не произойдет. Что касается альтернативных подходов, они будут работать, но не принесут столько плюсов, как официальное решение. Поэтому давайте посмотрим, какие именно плюсы нам обещают.

Создание стора


Перед тем как делать что-то со стором, нам нужно его создать. Вo Vuex 4, это выглядит следующим образом:

import { createStore } from 'vuex'export const counterStore = createStore({  state: {    count: 0  },    getters: {    double (state) {      return state.count * 2    }  },    mutations: {    increment (state) {      state.count++    }  },    actions: {    increment (context) {      context.commit('increment')    }  }})

Стор все также состоит из 4 частей: состояние (state), где хранятся данные; геттеры (getters), предоставляющие вычисляемые состояния; мутации (mutations), необходимые для изменения состояния и экшены (actions), которые вызываются за пределами стора для выполнения операций над ним. Обычно экшены не просто вызывают мутацию (как в примере), а используются для выполнения асинхронных задач (потому что мутации должны быть синхронными) или реализуют какую-то более сложную логику. Как же будет выглядеть Vuex 5?

import { defineStore } from 'vuex'export const counterStore = defineStore({  name: 'counter',    state() {    return { count: 0 }  },    getters: {    double () {      return this.count * 2    }  },    actions: {    increment () {      this.count++    }  }})

Первое, что изменилось переименование createStore в defineStore. Чуть позже будет понятно почему. Следующее, появился параметр name для указания имени стора. До этого, мы разделяли сторы на модули, а имена модулей были в виде именованных объектов. Далее модули регистрировались в глобальном пространстве, из-за чего они не были самодостаточными и готовыми для переиспользования. В качестве решения, нужно было использовать параметр namespaced, чтобы не давать нескольким модулям реагировать на тот же тип мутаций и действий. Думаю многие сталкивались с этим, но ссылку на документацию я, тем не менее, добавлю. Теперь у нас нет модулей, каждый стор по-умолчанию отдельное и независимое хранилище.

После указания имени, нам нужно сделать state функцией, которая возвращает начальное состояние, а не просто устанавливает его. Это очень похоже на то, как выглядит data в компонентах. Изменения коснулись и геттеров, вместо state как параметра функции мы используем this, чтобы получить доступ к данным. Тот же подход применен и к экшенам, this вместо statе как параметра. И наконец, самое главное, мутации объединены с экшенами. Kia отмечал, что мутации довольно часто становятся простыми сеттерами, делая их многословными, видимо это и послужило причиной удаления. Он не упоминает, можно ли будет производить изменение состояния за пределам стора, например из компонентов. Тут, мы можем только сослаться на Flux паттерн, который не рекомендует этого делать и поощряет подход с изменением состояния именно из экшенов.

Дополнение: те, кто использует Composition API для создания компонентов, будут рады узнать что, существует способ создания стора в похожей манере.

import { ref, computed } from 'vue'import { defineStore } from 'vuex'export const counterStore = defineStore('counter', {  const count = ref(0)  const double = computed(() => count.value * 2)    function increment () {    count.value++  }  return { count, double, increment }  })

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

Инициализация стора


Здесь нас ждут существенные изменения. Чтобы описать, как будет происходить инициализация стора в 5-ой версии, посмотрим как это происходит в 4-ой. Когда мы создаем стор через createStore, мы сразу же его инициализируем, чтобы затем использовать в app.use или напрямую.

import { createApp } from 'vue'import App from './App.vue'import store from './store'const app = createApp(App)app.use(store)app.mount('#app')// Теперь у всех компонентов есть доступ к `this.$store`// Или к `useStore()` в контексте Composition APIimport store from './store'store.state.count // -> 0store.commit('increment')store.dispatch('increment')store.getters.double // -> 4

В 5 версии, мы отдельно получаем доступ к каждому экземпляру Vuex, что дает гарантию независимости. Поэтому этот процесс выглядит иначе:

import { createApp } from 'vue'import { createVuex } from 'vuex'import App from './App.vue'const app = createApp(App)const vuex = createVuex()app.use(vuex)app.mount('#app')

Теперь все компоненты имеют возможность обращаться напрямую к любому экземпляру Vuex, вместо обращения к глобальному пространству. Взгляните на пример:

import { defineComponent } from 'vue'import store from './store'export default defineComponent({  name: 'App',  computed: {    counter () {      return this.$vuex.store(store)    }  }})

Вызов $vuex.store создает и инициализирует стор. Теперь, каждый раз общаясь к этому хранилищу через $vuex.store, вам будет возвращаться уже созданный экземпляр. В примере это this.counter, который мы можем использовать дальше в коде. Так же, можно инициализировать стор через createVuex().

И конечно, вариант для Composition API, где вместо $vuex.store используется useStore.

import { defineComponent } from 'vue'import { useStore } from 'vuex' // import useStoreimport store from './store'export default defineComponent({  setup () {    const counter = useStore(store)    return { counter }  }})

У описанного выше подхода (инициализация стора через компоненты) существуют как преимущества, так и недостатки. С одной стороны это разделение кода и возможность добавлять его только там, где это необходимо. С другой добавление зависимости (при том, что теперь нужно импортировать стор каждый раз, когда планируешь его использовать). Поэтому, если вы хотите использовать DI, то предлагается вариант с использованием provide:

import { createApp } from 'vue'import { createVuex } from 'vuex'import App from './App.vue'import store from './store'const app = createApp(App)const vuex = createVuex()app.use(vuex)app.provide('name', store)app.mount('#app')

И последующим пробрасыванием стора в компонент:

import { defineComponent } from 'vue'export default defineComponent({  name: 'App',  inject: ['name']})// Composition APIimport { defineComponent, inject } from 'vue'export default defineComponent({  setup () {    const store = inject('name')    return { store }  }})

Восторга от этого решения не так много, но оно выглядит более явно и гибко, чем текущий подход. Так же нельзя не сказать про дальнейшее использование. Сейчас это выглядит следующим образом:

store.state.count                // Statestore.getters.double            // Gettersstore.commit('increment')   // Mutationsstore.dispatch('increment')  // Actions

В новой версии ожидается:

store.count          // Statestore.double         // Gettersstore.increment()  // Actions

Все сущности (состояние, геттеры и экшены) доступны напрямую, делая работу с ними проще и логичнее. При этом убирая необходимость в использовании mapState, mapGetters, mapActions и mapMutations, а так же написания дополнительных вычисляемых свойства.

Совместное использование


Последние момент, который стоит рассмотреть компоновка. Мы помним, что во Vuex 5 у нас больше нет именованных модулей и каждый стор является отдельным и независимым. Это дает возможность импортировать их когда нужно и использовать данные по мере необходимости, прямо как компоненты. Появляется логичный вопрос, как использовать несколько сторов вместе? В 4 версии все еще существует глобальное пространство имен и нам нужно использовать rootGetters и rootState, чтобы обращаться к разным сторам в этой области (так же как и в 3 версии). Подход в Vuex 5 иной:

// store/greeter.jsimport { defineStore } from 'vuex'export default defineStore({  name: 'greeter',  state () {    return { greeting: 'Hello' }  }})// store/counter.jsimport { defineStore } from 'vuex'import greeterStore from './greeter'export default defineStore({  name: 'counter',  use () {    return { greeter: greeterStore }  },    state () {    return { count: 0 }  },    getters: {    greetingCount () {      return `${this.greeter.greeting} ${this.count}'    }  }})

Мы импортируем стор, затем регистрируем его через use, и тем самым получаем к нему доступ. Все выглядит еще проще если использовать Сomposition API:

// store/counter.jsimport { ref, computed } from 'vue'import { defineStore } from 'vuex'import greeterStore from './greeter'export default defineStore('counter', ({use}) => {  const greeter = use(greeterStore)  const count = 0  const greetingCount = computed(() => {    return  `${greeter.greeting} ${this.count}`  })  return { count, greetingCount }})

Единственное, что стоит здесь упомянуть, это use, который работает точно также как vuex.store и отвечает за правильную инициализацию сторов.

Поддержка TypeScript


Благодаря изменениям в API и уменьшению количества абстракций, поддержка TypeScript в 4 версии будет намного лучше, но нам все еще потребуется много ручной работы. Выход же 5 версии даст возможность добавлять типы, там где необходимо, и там где мы этого хотим.

Заключение


Vuex 5 выглядит многообещающе и именно так как многие и ожидают (устранение старых недочетов, добавление гибкости). Полный список обсуждений и мнения основной команды можно найти в Vue RFCs репозитории.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru