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

В данной статье будут рассмотрены базовые принципы работы с пользовательскими событиями в 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(); //отменяем перезагрузку страницы

                                 //получаем данные свойства 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: { //массив с пользователями 

        container: "user",
        props: [ "user_name", ],
        methods: {        
        },          
    },
}
window.onload = function(){//создаем экземпляр приложения htmlix

    var 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 и вызывать его при изменении данных:

stateMethods: {

           //this в stateMethods методах указывает на rootLink
         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(name); ///вызываем метод передав ему контекст= HM
}

//в форме
click: function(){
                  event.preventDefault();
                  var text = this.parent.props.input.getProp();
                  this.rootLink.stateMethods.entryUser(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"].methods.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" можно посмотреть здесь.