Создание прототипа SPA интернет магазина

В этом разделе будет описано пошаговое создание простого прототипа фронтенда SPA интернет магазина. Серверная часть которого написана на node.js с использованием express, шаблонизатора - twig , базы данных - NeDB , для обработки данных форм - модуль formidable , клиентская часть написана на htmlix.

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

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

var State = {    
}
    window.onload = function(){
            ///создаем экземпляр  HTMLix

        var HM = new HTMLixState(State);
            console.log(HM);
    }

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

  • сперва нужно установить все модули которых сейчас нет, для этого введем в консоли: npm install
  • далее ввести в командной строке node app
  • затем перейти по адресу 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="/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="/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="/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' %}

Аналогично для адреса http://localhost:3000/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.

var State = {

    categories: { //компонент категории
        container: "category", //контейнер компонента категории
        props: [ ],
        methods: {

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

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

  • "data" - данные которые содержат idCategory ( {{ category.idCategory }} ),
  • "click"- обработчик кликов по категории,
  • 'class' - доступ к классу категории для изменения цвета текущей категории,
  • "title" - доступ к тексту внутри категории ( {{ category.titleCategory }} ),

В 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);

            },
        }       
    },

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

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

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

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

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

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);

    },
    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){ 
    //метод вызывает fetchCategoryCarts передавая в него функцию обратного вызова 
    // в которой подставляем данные с сервера в переменную carts объекта stateProperties
    //в дальнейшем также будем вызывать событие "emiter-load-carts" которое мы еще не создали
    //this в методах из объекта stateMethods указывает на rootLink

       var context = this;

       this.stateMethods.fetchCategoryCarts(url, function(data){

           context.stateProperties.carts = data;

           console.log(data)
          //context.eventProps["emiter-load-carts"].setEventProp(data);                

        });
    },

 },
 eventEmiters: {//создали объект со всеми пользовательскими событиями

     ["emiter-click-on-category"] : { //добавили событие "emiter-click-on-category"                
        prop: [],               
     },
  }        
}
window.onload = function(){
    ///создаем экземпляр  HTMLix

    var HM = new HTMLixState(State);
    console.log(HM);
}

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

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

Далее давайте добавим свойства контейнера 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:

 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;

         for(var i=0; i<carts.length; i++){
         //в цикле перебираем полученный массив с карточками товара 

         var cart = carts[i];

           var props = { //создаем объект со всеми свойствами для контейнера cart

            title: 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",
    //добавили все свойства для контейнера cart
    props: ['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 ){

       var context = this;

       this.stateMethods.fetchCategoryCarts(url, function(data){

           context.stateProperties.carts = data;
           context.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"]: {  //знак `:` говорит что это параметр, и с ним сравнение не требуется, проверяется только его наличие.

     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" - событие клика по категории в карточке товара,
  • "listner_click_on_cart" слушатель пользовательского события - "emiter-click-on-cart",
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", "data"
               ],

   methods: {

    listner_click_on_cart: function(){

        var index = this.emiter.getEventProp(); 
                 //получаем индекс контейнера карточки по которой кликнули в компоненте carts

        var 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: 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
    //устанавливает компонент carts  в div теге  `data-router_carts="router"` 
     /* ............ конец метода ............*/
     this.rootLink.router.setRout(historyUrl);    
}

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

window.onload = function(){

    ///создаем экземпляр  HTMLix
    var 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/  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 компонента - контейнера cart_single чтобы при клике по категории изнутри карточки товара у нас также менялась активная категория и изменялся отображаемый в роутере компонент, однако чтобы не дублировать код, так как он будет такой-же как и в компоненте category мы унаследуем из него данный метод, а старый удалим.

 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", ""], "data", //"click", 
        ["click", "extend", "category", "props"]///наследуем свойство из компонента "category"
   ],

/* закомментировали метод
    click: function(){
       event.preventDefault(); 
        console.log(this.parent.props.data)                
     } */ 

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

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);

           //меняем компонент в div теге  `data-router_carts="router"` - на carts 
           this.rootLink.router.setRout(historyUrl);    

    },
    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: {
    //добавили свойство 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;

        for(var i=0; i<carts.length; i++){
         //в цикле перебираем полученный массив с карточками товара 

           var cart = carts[i];

           var props = { //создаем объект со всеми свойствами для контейнера cart

            title: 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",
    //добавили все свойства для контейнера cart
    props: ['title', "title_category",  "cost", "click", "src_img", "data"  ],
    methods: {
         click: function(){ //метод для обработки кликов по карточке товара, 

           event.preventDefault();                
           var url = this.parent.props.data.getProp(); //в свойстве data контейнера cart у нас url крточки товара.
           console.log(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);  


        }
    }     
 },
 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", ""], "data", //"click",
       ["click", "extend", "category", "props"]///наследуем свойство из компонента "category"
   ],

   methods: {

    listner_click_on_cart: function(){

        var index = this.emiter.getEventProp(); 
                 //получаем индекс контейнера карточки по которой кликнули в компоненте carts

        var 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: function(){
          event.preventDefault(); 
           console.log(this.parent.props.data)                
     } */ 
   },
},
cart_variant_option: {
     container: "cart_variant_option", // views/twig_templates/  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());
        }

    }

  },
  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){ 
    //метод вызывает fetchCategoryCarts передавая в него функцию обратного вызова 
    // в которой подставляем данные с сервера в переменную carts объекта stateProperties
    //this в методах из объекта stateMethods указывает на rootLink

       var context = this;

       this.stateMethods.fetchCategoryCarts(url, function(data){

           context.stateProperties.carts = data;

           context.eventProps["emiter-load-carts"].setEventProp(data);                

        });
    },

 },
 eventEmiters: {//создали объект со всеми пользовательскими событиями

     ["emiter-click-on-category"] : { //добавили событие "emiter-click-on-category"                
        prop: [],               
     },
     ["emiter-load-carts"]: { 
        prop: "",
     },
     ["emiter-click-on-cart"]: {
        prop: "",
     },  
  }        
}

var routes = {

 ["/"]: {        
    first: ["categories", 'carts'],  /// компоненты которые есть в html файле указываются в этом массиве, остальные будут загружены с шаблона, в fetch запросе асинхронно

    routComponent: { 
        router_carts: "carts", //компонент соответствующий данному роуту
    }, 
    templatePath: "/static/templates/index.html" // папка для загрузки шаблонов
 },            
 ["/category/:idCategory"]: {  //знак `:` говорит что это параметр, и с ним сравненние не требуется, проверяется только его наличие

    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(){

    ///создаем экземпляр  HTMLix
    var 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);
}