Папки:
dist: htmlix.js with sourcemap
htmlix-components: исходные файлы фреймворка
Htmlix - яваскрипт фреймворк основанный на data- свойствах html документа.
Особенности:
- объектно-ориенитрованный подход построения приложения, наличие структуры и иерархии компонентов, возможность наследования свойств компонентов,
- пользовательские события для обновления интерфейса и передачи данных между компонентами,
- встраивается в уже отданную сервером страницу, не требует серверного рендеринга,
- возможность строить приложения используя роутер и html шаблоны,
- типизация свойств, сокращает количество используемых методов и уменьшает объем кода требуемого для написания и запоминания.
Примеры приложений на Htmlix:
- Collage_n онлайн редактор для создания коллажей и спрайтов из картинок
- htmlix api в SPA формате
- Прототип SPA интернет магазина
- тестовые приложения для htmlix
Для создания приложения Htmlix необходимо создать экземпляр new HtmlixState( StateMap) передав в него объект с описанием приложения StateMap. В описании приложения StateMap находятся все компоненты, эмитеры событий, общие методы и переменные всего приложения, а таже другие настройки...
Компонентами в Htmlix выступают контейнеры и массивы. Контейнер это html элемент с различным набором свойств. Массив это html элемент который является хранилищем для однотипных контейнеров. Свойство это обьект имеющий доступ к html данным например тексту или атрибуту, также свойство может быть слушателем события.
Все свойства имеют строго определенный тип данных, например "text" - текстовые данные, "src" - src атрибут, также свойство может быть слушателем события.
Контейнер
Для того чтобы создать контейнер необходимо указать его название и определить тип = "container" в html теге. Создадим контейнер page:
<div data-page="container"> ..... </div>Далее в описании приложения ( StateMap ) создать компонент контейнер указав его название и другие параметры:
var StateMap = {
page: { //название компонента
container: "page", //название контейнера
props: [], //массив свойств
methods: { //методы для свойств - обработчиков событий
}
}
}
//Теперь создадим экземпляр приложения :
window.onload = function(){
var HM = new HtmlixState(StateMap);
console.log(HM);
}Открыв консоль можно увидеть созданный экземпляр приложения.
В нем содержится несколько объектов это:
description- описание приложения (обьект StateMap);eventProps- обект для доступа к пользовательским Emiter событиям;state- доступ ко всем компонентам приложения;
Также здесь могут быть и другие поля:
stateMethods- общие методы для всего приложения;stateProperties- общие переменные для всего приложения;stateSettings- настройки приложения;
Далее открыв обьект state в нем будет находиться наш компонент page кликнув по нему можно увидеть все остальные поля контейнера:
htmlLink- ссылка на Html обьект;index- порядковый номер контейнера в массиве, в нашем случае он равен null потому что он не помещен в массив, а является компонентом;name- имя контейнера в нашем случае это "page";pathToCоmponent- имя компонента совпадает с именем контейнера, т.к. контейнер сам является компонентом (не помещен в массив);props- объект со свойствами (Prop) сейчас он пустой так как мы их еще не создали;renderType- тип контейнера "container-outer" говорит что контейнер является - компонентом, "container-inner" - что он в массиве;rootLink- ссылка к корневому элементу (экземпляру приложения);type- "container" - тип объекта - контейнер (мы его указали в div элементе);
Также здесь могут быть дополнительные поля:
-
если контейнер в свойстве с типом
group: -
groupParent- доступ из контейнера в свойство; -
groupId- индекс контейнера в группе; -
если контейнер в свойстве с типом
render-variant: -
renderParent- доступ из контейнера в свойство
Теперь рассмотрим методы контейнера:
this.component()- возвращает компонент для данного контейнера если он в массиве то вернет массив (pages), если контейнер сам является компонентом то вернет this;this.remove(withChild)- удаляет контейнер, если передать параметром whithChild=true удаляет также дочерние контейнеры, находящиеся в свойствах с типами: "group" и "render-variant", можно удалить только контейнер находящийся в массиве (renderType="contaiter-inner");this.setAllProps(mapObject)- проверяет обектmapObjectна наличие совпадающих ключей со свойствами контейнера и устанавливает их значение для всех совпавших;.getAllProps(mapObject)- возвращает данные со всех свойств названия которых совпали с названиями ключей в объекте mapObject путем вызова метода getProp(obg) на кождом совпавшем свойстве, например mapObject={key1: "", key2: {ke1: ""} } вызовет key1.getProp(), key2.getProp({ke1: ""}). Таким образом можно получить развернутый объект с ключами которого будут имена свойств а значениями - данные этих свойств, если не передать в метод mapObject то создаст объект со всеми свойствами из данного контейнера.
Свойства это объекты имеющие доступ к свойствам html страницы, также свойства могут быть слушателями событий. Для создания свойства необходимо указать его имя после имени контейнера, а также указать тип данного свойства. Давайте создадим несколько свойств: my_class, paragraf и btn_click в контейнере page:
<style type="text/css">
.new_class { color: red; }
</style>
<div data-page="container">
<p data-page-paragraf="text" data-page-my_class="class"> <!-- свойство paragraf с типом "text" и my_class с типом "class" -->
текст
<p>
<button data-page-btn_click="click">Кнопка</button> <!-- btn_click - свойство - слушательсобытия "click" -->
</div>Название свойства в html идет после названия контейнера и знака "-" (page-), в самом названии свойства знак "-" использовать нельзя. Далее после знака = идет тип свойсва, у нас три разных типа это "text", "class" и "click". Если тип свойства является событием то в описании приложения, в объекте methods для данного свойства необходимо указать одноименный метод с обработчиком события.
Далее в описании приложения:
var StateMap = {
page: {
container: "page",
props: ["paragraf", "my_class", "btn_click"], //создали три свойства в контейнере page
methods: {
btn_click: function(){ //одноименный метод для свойства - события;
console.log(this);
this.parent.props.paragraf.setProp("Новый текст");
//this.parent - доступ из конкретного свойства в контейнер
this.parent.props.my_class.setProp("new_class");
}
}
}
}
В примере выше мы указали свойства с помощью дата атрибутов, также свойства можно указывать с помощью массива, чтобы не писать их все в разметке, либо комбинировать два способа.
Итак мы создали три свойства одно из которых обработчик события "click". this - в методе указывает на свойство к которому прикреплен данный обработчик - btn_click. Далее с помощью .parent.props мы получаем доступ к контейнеру а далее ко всем свойствам контейнера, затем по имени свойства к конкретному свойству. Метод setProp является универсальным и работает по разному в зависимости от типа свойств, для тима "text" он меняет текст, а для типа "class" он добавляет класс.
После нажатия кнопки изменился текст и добавился новый класс. Далее давайте посмотрим в консоль. После клика в ней появился обьект Prop.
Свойства контейнера - находятся в обьекте props;
В зависимости от типа здесь могут присутствовать разные поля:
- Поля для всех типов свойств:
type- тип свойства в данном случае это "click";htmlLink- html ссылка на данное свойство;
Дополнительное поле для свойства с типом data:
parent- доступ к контейнеру из данного свойства;
Если свойство является событием, группой или вариантом(render-variant):
propName- название свойства;parent- доступ к контейнеру из данного свойства;pathToCоmponent- имя компонента (не путать с именем контейнера т. к. они могут быть разными если компонент является массивом);prop- какие либо произвольные данные, по умолчанию null;rootLink- корневая ссылка на экземпляр приложения HtmlixState;
Дополнительное поле для стандартных событий:
events- объект с методами обработчиками событий для свойства, доступ по имени события;
Дополнительные поля для пользовательских событий:
emiterKeyключь данного свойства для доступа к обработчику пользовательского события, генерируется случайным числом.emiterдоступ к пользовательскому событию из свойства;
Дополнительные поля для типа group:
groupChild- массив с контейнерами данной группы;groupArray- ссылка на виртуальный массив контейнеров данной группы;
Дополнительное поле для типа render-variant:
renderChild- ссылка на отображаемый компонент в данном свойстве, либо на контейнер виртуального массива.
В каждом свойстве есть три основных метода это:
.setProp("Новые данные")- добавляет либо изменяет данные свойства;.getProp()- получает данные свойства, например свойство с типом "src" получит адрес ссылки;.removeProp()- удаляет данные из данного свойства, для каждого типа работает по разному, например для класса необходимо указать имя класса для удаления .removeProp("name_class")
а также:
.component()- возвращяет компонент, в котором находится свойство, если это контейнер не помещенный в массив то можно использовать .parent
-
Навигация по свойствам внутри одного общего контейнера как вы уже наверное заметили осуществляется с помощью
this.parent- здесь this - указывает на свойство в обработчике события которого мы находимся, далее parent - это родительский контейнер. -
this.parent.props.name_prop- так можно попасть из одного свойства в общем контейнере в другое, где props это все свойства какого либо контейнера. -
Если свойство расположено не в контейнере а в массиве то
parent- указывает на массив в котором оно расположено. -
Перейти из массива в свойства контейнера можно так:
this.parent.data[index контейнера].props.имя_свойства. Где data - это массив с контейнерами. -
Попасть из контейнера в массив
this.rootLink.state[this.pathToComponent]либо вызвав this.component(); -
Доступ к любому компоненту из любой точки приложения осуществляется
this.rootLink.state["имя_компонента"] -
Доступ сразу в массив из свойства контейнера (минуя контейнер) возможно так
this.component() -
Напомню что компонентом может быть как массив так и самостоятельный контейнер, не помещенный в массив, поэтому вызов this.component() в контейнере не помещенном в массив просто вернет this
-
Доступ к пользовательским событиям из любой точки осуществляется с помощью
this.rootLink.eventProps["emter-имя-события"]далее getEventProp() или setEventProp(new_prop) -
Доступ к пользовательскому событию из метода подписчика осуществляется так:
this.emiterдалее getEventProp или setEventProp -
Названия пользовательских событий желательно начинать со слова "emiter".
Также навигацию можно осуществлять с помощью методов сокращенного доступа
Далее давайте поместим наш контейнер в массив а также добавим в контейнер кнопку удаления.
<div data-pages="array" style="border: 1px solid red; padding: 10px;">
<!-- создали массив pages и поместили в него два одинаковых контейнера page -->
<div data-page="container" style="border: 1px solid green">
<p data-page-paragraf="text" data-page-my_class="class">текст<p>
<button data-page-btn_click="click">Кнопка</button>
<button data-page-remove="click">Удалить</button>
<!-- добавили кнопку удаления для контейнера page и поместили в нее свойство "remove" -->
</div>
<div data-page="container" style="border: 1px solid green">
<p data-page-paragraf="text" data-page-my_class="class">текст<p>
<button data-page-btn_click="click">Кнопка</button>
<button data-page-remove="click">Удалить</button>
</div>
</div>
Тперь изменим описание приложения:
var StateMap = {
pages: { //теперь компонент называется pages
container: "page", //названия контейнеров не поменялись
props: ["paragraf", "my_class", "btn_click", "remove"], //добавили свойство "remove"
methods: {
btn_click: function(){
console.log(this);
this.parent.props.paragraf.setProp("Новый текст");
this.parent.props.my_class.setProp("new_class");
},
remove: function(){ //обработчик события для свойства "remove"
this.parent.remove(); //получаем доступ к контейнеру из свойства, а затем удаляем контейнер
}
}
}
}Итак после того как мы поместили контейнер в массив pages компонент принял название массива, а названия для контейнеров остались прежними; Также теперь контейнер можно удалить т. к. он находится в массиве.
Давайте откроем консоль в ней экземпляр приложения и перейдем по навигации state.pages к компоненту pages и рассмотрим его поля:
data- содержит все контейнеры массива, порядковый номер контейнера в массиве совпадает с полем index контейнера, после добавления или удаления контейнера из массива индекс других смежных контейнеров может измениться;htmlLink- html ссылка на массив;index- null для массива;pathToComponent- название компонента (pages);renderType- тип отображения "array" также может быть "virtual-array" для виртуального массива;rootLink- ссылка на корневой объект;selector- уточняющий селектор для поиска контейнеров относительно тега массива, например "div:last-of-type" - будет искать последний div внутриtemplateData- шаблон для создания нового контейнера для данного массива, берется первый из массива, если указать тип контейнера "template", то при инициализации делается с него копия, а шаблон затем удаляется из массива;type- тип "array";
Давайте теперь изменим текст первого контейнера нажав на "кнопку" а затем удалим его нажав на кнопку "удалить", нулевой контейнер удалится, а первый поменяет индекс на ноль таким образом порядок в массиве сохранится. Теперь если прейти по навигации pages.data[0].index будет равным "0" в то время как до удаления нулувого контейнера он был равен "1";
Далее рассмотрим динамическое создание новых контейнеров page в массиве pages. Для этого создадим новый компонент - форму в которой будем создавать новые контейнеры:
<!-- создали новый компонент create_page -->
<form data-create_page="container" style="border: 1px solid blue; padding: 10px; margin: 10px;">
<div class="form-group">
<label for="container_text">текст записи</label>
<textarea data-create_page-text="inputvalue" name="container_text" id="container_text" rows="1"></textarea>
<!-- свойство text с типом данных "inputvalue" -->
</div>
<button data-create_page-create="click">Создать</button>
<!-- свойство create с типом данных "click" -->
</form>
<div data-pages="array" style="border: 1px solid red; padding: 10px;">
<!-- массив pages без изменений (см. пример выше ##Массив) -->
</div>
Теперь создадим новый компонент - create_page в описании приложения:
var StateMap = {
create_page: { // добавили новый компонент create_page
container: "create_page", // имя контейнера совпадает с именем компонента, так как сам контейнер является компонентом
props: ["text", "create"], // добавили два свойства
methods: {
create: function(){ // для свойства- события добавили одноименный обработчик
event.preventDefault(); // отменяем перезагрузку страници
var text = this.parent.props.text.getProp();
// получаем данные свойства находящегося в том же контейнере
this.rootLink.state["pages"].add({paragraf: text});
// создаем новый контейнер page в компоненте pages с полученными данными формы
}
}
},
pages: {
<!-- компонент pages без изменений (см. пример выше ##Массив) -->
}
}Итак мы создали компонет create_page для создания новых страниц page c помощью метода массива .add(). Давайте более подробно разберем метод .add() а также другие методы массива:
.add(props, insertLocation)- создает новый контейнер со свойствами props в указанной позиции "insertLocation" в массиве, если не указать позицию то создаст контейнер в конце массива;.removeIndex(indexArray, widthChild)- удаляет несколько контейнеров из массива, indexArray - массив с индексами контейнеров для удаления, widthChild - если передать параметром whithChild=true удаляет также дочерние контейнеры, находящиеся в свойствах с типами: "group" и "render-variant";.removeAll( widthChild)- удаляет все контейнеры из массива, widthChild - если передать параметром whithChild=true удаляет также дочерние контейнеры, находящиеся в свойствах с типами: "group" и "render-variant";.reuseAll(arrayWithObjects)- переиспользует уже существующие в массиве контейнеры, меняя их props совпавшие с ключами объектов из массива arrayWithObjects, если новых объектов меньше чем контейнеров то удаляет лишние контейнеры, если больше то добавляет. Бывает полезен для того чтобы не очищать постоянно массив, и не добавлять новые контейнеры, там где этого не требуется (чтобы не мигала картинка);.getAll(map_Object)- возвращает массив объектов ключами которых являются свойства контейнеров, а значениями данные полученые из метода getProp(), для каждого свойства, map_Object - список только необходимых свойств, если не передать получит все свойства. Работает на основе метода контейнера getAllProps;.order(newOrderArr)- изменяет порядок контейнеров в массиве и html разметке на новый, newOrderArr- массив со 'старыми' индексами контейнеров в новом порядке, например [3,2,0,1];
При создании массива в нутри него обязательно должен быть шаблон контейнера.
Он помещается в поле .templateData массива.
Берется этот шаблон из первого контейнера (с индексом 0) и сохраняется в данном поле.
Но что если мы хотим создать пустой массив, а только в будущем добавлять туда контейнеры.
Для этого создается массив с контейнером-шаблоном, который после создания массива автоматически удаляется из него.
Например:
<div data-some_arr="array">
<div data-some_cont="template" style="display: none">
контейнер - шаблон, удалится сразу после инициализации приложения
style="display: none" в самом шаблоне изменится на style="display: '' "
</div>
</div>
Указывается style="display: none" - для того чтобы шаблон не было видно, пока он еще не удалился.
Пользовательское событие это событие начинающееся со слова "emiter-" они нужны для создания динамических переменных, чтобы слушатели обновляли свой интерфейс на основе новых данных. Итак в нашем компоненте pages постоянно создаются и удаляются новые контейнеры, соответственно их index постоянно меняется, давайте создадим событие "emiter-create-page", которое будут слушать все контейнеры page и обновлять свое свойство page_index которое мы также создадим;
Добавим в html код из примера выше (#Добавление контейнеров в массив) контейнеров "page" свойство - событие listener_create_page с названием события "emiter-create-page" и свойство page_index с типом "text":
<form data-create_page="container" style="border: 1px solid blue; padding: 10px; margin: 10px;> <!-- ...без изменений... --> </form>
<div data-pages="array" style="border: 1px solid red; padding: 10px;">
<div data-page="container" data-page-listener_create_page="emiter-create-page" style="border: 1px solid green">
<!-- добавили свойство - слушателя события "emiter-create-page" -->
<p data-page-paragraf="text" data-page-my_class="class">текст<p>
<p>index= <span data-page-page_index="text" > 0</span> </p>
<!-- добавили свойство page_index для отображения меняющихся данных -->
<button data-page-btn_click="click">Кнопка</button>
<button data-page-remove="click">Удалить</button>
</div>
<!-- аналогично и для второго контейнера -->
<div data-page="container" data-page-listener_create_page="emiter-create-page" style="border: 1px solid green">
<p data-page-paragraf="text" data-page-my_class="class">текст<p>
<p>index= <span data-page-page_index="text" > 1</span> </p>
<button data-page-btn_click="click">Кнопка</button>
<button data-page-remove="click">Удалить</button>
</div>
</div>
Теперь изменим описание приложения:
var StateMap = {
create_page: {
container: "create_page",
props: ["text", "create"],
methods: {
create: function(){
event.preventDefault();
var text = this.parent.props.text.getProp();
this.rootLink.state["pages"].add({paragraf: text}, 0); //добавляем контейнер в начало массива, соответственно индекс всех остальных увеличивается на 1
this.rootLink.eventProps["emiter-create-page"].emit();
//вызвали пользовательское событие "emiter-create-page" при создании контейнера
}
}
}, pages: {
container: "page",
//добавили свойства "page_index" и "listener_create_page"
props: ["paragraf", "my_class", "btn_click", "remove", "page_index", "listener_create_page"],
methods: {
btn_click: function(){
console.log(this);
this.parent.props.paragraf.setProp("Новый текст");
this.parent.props.my_class.setProp("new_class");
},
remove: function(){
this.parent.remove(); //удалили контейнер соответственно индекс контейнеров идущих после него уменьшился на 1
this.rootLink.eventProps["emiter-create-page"].emit();
//вызвали пользовательское событие "emiter-create-page" при удалении контейнера
},
// добавили обработчик события "emiter-create-page" для свойства listener_create_page всех контейнеров
listener_create_page: function(){
this.parent.props.page_index.setProp( this.parent.index );
//обновили интерфейс всех контейнеров на основе меняющегося index
}
}
},
eventEmiters: { //создали объект со всеми пользовательскими событиями приложения
["emiter-create-page"]: { //наше событие с начальными данными
prop: "",
}
}
}Итак после каждого создания либо удаления контейнера page мы вызываем событие "emiter-create-page" и все подписчики обновляют свои данные; Теперь если создать новый контейнер он получит индек равный 2, азатем удалить нулевой контейнер с инедексом 0 то созданный нами контейнер изменит индекс с 2 на 1 и мы с помощью пользовательского события обновим его интерфейс. Также можно передавать новые данные в пользовательское событие которые затем получат все слушатели, давайте разберем подробнее все методы объекта eventProps["emiter-name"]
Доступ из любой точки приложения осуществляется по имени пользовательского события:
this.rootLink.eventProps["emiter-имя-события"]
Доступ из свойства - слушателя события осуществляется: this.emiter
-
.emit()- вызывает событие для всех слушателей; -
.setEventProp("новые данные")- вызывает событие для всех слушателей и меняет переменную this.rootLink.eventProps["emiter-имя-события"].prop на новые данные, получить новые данные в слушателе события можно с помощью this.emiter.prop или this.emiter.getEventProp(); -
.getEventProp()- получает данные пользовательского события; -
behavior()метод добавляется к эмитеру событий, если возвращает false событие не будет вызвано, пример использования:
eventEmiters: {
//событие для смены типа навигации
["emiter-navigation-type"] : {
prop: "",
behavior: function(){
//если ширина экрана меньше 600 px событие не сработает
if(window.innerWidth < 600 && this.prop == "top-menu")return false;
this.rootLink.stateProperties.NAVIGATION_TYPE = this.prop;
return true;
}
},
},
Слушателями пользовательских событий могут быть, как свойства Prop контейнера Container, так и свойства Массива Array;
Итак давайте разберем отличие свойств контейнера от свойств массива.
Например у нас есть массив menu со свойствами class_menu и listener_load_page в котором расположены с конейнеры item со свойствами class_item и text_item:
<div data-menu="array" data-menu-class_menu="class" data-menu-listener_load_page="emiter-load-page">
<div data-item="container" data-item-class_item="class">
<a data-item-text_item="text"> текст 1 </a>
</div>
<div data-item="container" data-item-class_item="class">
<a data-item-text_item="text"> текст 2 </a>
</div>
</div> В описании приложения:
var State ={
menu: {
arrayProps: [ "class_menu", "listener_load_page" ], //свойства массива
arrayMethods: {
listener_load_page: function() { //обработчик события для свойства массива
this.parent.add( this.emiter.getEventProp() );
//this.parent указывает на массив
}
},
container: "item", //название контейнера
props: [ "class_item", "text_item"], // свойства контейнера
methods: {
}
},
eventEmiters: {
["emiter-load-page"]: { //пользовательское событие с начальными данными
prop: "",
}
}
}
window.onload = function(){
var HM = new HtmlixState(State);
window.setTimeout( function(){
HM.eventProps["emiter-load-page"].setEventProp( {text_item: "новый текст"} );
console.log(HM);
}, 3000 );
} Как видно из примера выше свойство class_menu и listener_load_page является свойствами массива (arrayProps - в описании приложения), а свойства class_item и text_item - свойствами контейнера item (props - в описании приложения).
В созданном экземпляре приложения вызов this.parent в свойстве контейнера указывает на контейнер, а вызов this.parent в массиве - на массив в котором оно расположено.
Важно правильно размещать свойства - слушателей пользовательских событий, т.к. при размещении его в контейнере, оно вызывается на каждом контейнере, а при размещении в массиве, вызывается только один раз для массива. Поэтому добавлять контейнеры в массив лучше из свойства массива, как в примере выше.
Существует два способа указания свойств:
-
1 - Указываем свойство в html разметке с помощью
data-container_name-prop_name, либоdata-array_name-prop_name- для свойст массива. Далее в описании приложения указываем его в массиве props:["prop_name", ...], либоarrayProps: ["prop_name", ...]- для свойств массива. -
2 - Указываем свойство только в описании приложения с помощью массива
props:[ ["prop_name", "prop_type", "selector"] ]. где "selector" - селектор для поиска свойства относительно контейнера, либо массива - для свойств массива, например"a:first-of-type". Если селектор указать пустым""это будет означать что свойство является в том-же теге что и контейнер, либо массив - для свойств массива.
Из примера выше свойства можно указать так:
-
для свойств массива:
arrayProps:[ ["class_menu", "class", ""], [ "listener_load_page", "emiter-load-page", "" ] ]- тотже тег что и у массива; -
для свойств контейнера:
props:[ ["class_item", "class", ""], ["text_item", "text", "a:first-of-type"] ]a:first-of-type - селектор относительно контейнера
Теперь в html разметке у нас останется только массив и два контейнера:
<div data-menu="array" >
<div data-item="container" >
<a > текст 1 </a>
</div>
<div data-item="container" >
<a > текст 2 </a>
</div>
</div> Из примера выше (#Отличие свойств контейнера от свойств массива) , добавим в массив какой-нибудь параграф, а контейнеры поместим в другой div элемент с классом "new_div"
<div data-menu="array" data-menu-listener_load_page="emiter-load-page" data-menu-class_menu="class">
<div class="new_div">
<div data-item="container" data-item-class_item="class">
<a data-item-text_item="text"> текст 1 </a>
</div>
<div data-item="container" data-item-class_item="class">
<a data-item-text_item="text"> текст 2 </a>
</div>
</div>
<div>
<p> какой-то текст</p>
</div>
</div> Теперь запустив код можно заметить что после трех секунд ожидания новый контейнер появился ниже параграфа с "каким-то" текстом. Это происходит потому что контейнеры добавляются в тег который содержит data-menu="array".
Давайте уточним место их нахождения относительно массива menu в описании приложения:
var State ={
menu: {
selector: "div:first-of-type",
/* далее без изменений */
Теперь запустив пример новый контейнер появляется там где надо.
Для создания любого свойства Prop необходимо в описании приложения либо в html коде указать тип данного свойства.
При инициализации приложение определит тип данного свойства и на основании его создаст объект Prop.
Если тип свойства является стандартным событием например "click", 'mouseup' и т. д., то к нему будет присоединен обработчик события который необходимо создать
в объекте method для данного контейнера в описании приложения.
Если тип свойства является пользовательским событием, то также как и для обычного события создается обработчик.
В обработчиках событий оператор this - указывает на данный конкретный экземпляр Prop, а далее с помощью навигации можно переходить к любым другим свойствам относительно данного.
Методы setProp("newProp"), getProp() и removeProp() работают по разному в зависимости от типа свойства.
Перечень всех типов:
- "text" - текстовые данные;
- "html" - html разметка;
- "inputvalue", "select" - данные форм ;
- "checkbox", "radio" - чекбоксы и радио;
- "class" - массив с классами;
- "render-variant" - текущий отображаемый объект;
- "group" - группа контейнеров из какого либо виртуального массива;
- "group-mix" - группа контейнеров из разных виртуальных массивов;
тип data - т. к. в данных с типом дата после знака = идут какие либо данные, то для создания данного типа после имени массива или контейнера пишется имя data а далее уже какие либо данные, например: data-page-data_name="какие либо данные", здесь тип данных определяется по имени свойства оно всегда должно начинаться с data после названия контейнера.
Атрибуты: 'alt', 'disabled', 'href', 'id', 'src', 'style', 'title'.
Типы стандартных событий: 'click', 'keydown', 'dblclick', 'contextmenu', 'selectstart', 'mousewheel', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mousedown', 'keypress', 'keyup', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'abort', 'change'.
тип aux - вспомогательный метод: ["имя_метода", "aux"] в описании приложения объявляется в массиве свойств - props, и также как для обработчика событий для него указывается
метод в объекте methods. this в нем указывает на контейнер либо массив (если он объявлен в свойствах массива). Может принимать параметры и возвращвть значение.
В уже созданном компоненте (контейнере или массиве) он будет доступен в объекте methods контейнера или массива (в объекте props его не будет).
Объявлять можно только с помощью массива ["имя_метода", "aux"], в html разметку, не добавляется. Наследуется по тому-же принципу как и обычные свойства.
Пример использования - см. ниже # Добавление вспомогательных методов.
Рассмотрим как работает метод getProp() для основных типов свойств:
-
"text" - текстовые данные - возвращает this.htmlLink.textContent;
-
"html" - разметка - возвращает this.htmlLink.innerHtml;
-
"inputvalue", "select" - данные форм возвращает - this.htmlLink.value;
-
"checkbox", "radio" - чекбоксы и радио возвращает tru или false - this.htmlLink.checked;
-
"class" - возвращает массив с классами this.htmlLink.classList;
-
"render-variant" - если в свойстве отображается контейнер вызывает метод .getAllProps(), для массива вызывает метод .getAll(), тем самым получает значения всех свойств из отображаемого элемента, если передать в параметром mapObject={key1: "", key2: {ke1: ""} }
.getProp(mapObject), вернет свойства только совпадающие по названиям с key, таким образом можно получить развернутый объект со всеми значениями свойств либо со всеми нужными значениями свойств из отображаемого элемента; -
"group" - возвращает массив с объектами ключами которых будут названия свойств, а значения, данные полученные из этих свойств на каждом контейнере, использует метод this.groupChild[i].getAllProps(), если передать параметром mapObject={key1: "", key2: {ke1: ""} }
.getProp(mapObject), вернет свойства только совпадающие по названиям с key, таким образом можно получить развернутый объект со всеми значениями свойств либо со всеми нужными значениями свойств из дочерних контейнеров; -
"group-mix" - тип для хранения контейнеров из различных виртуальных массивов в одном свойстве;
-
'href', 'src', "id", "style" и другие атрибуты возвращает данный атрибут this.htmlLink.getAttribute(this.type);
-
для событий обычно не используется, возвращает this.type;
тип data - т. к. в данных с типом дата после знака = идут какие либо данные, то для создания данного типа после имени массива или контейнера пишется имя data_name а далее уже какие либо данные, например: data-page-data_page_id="какие либо данные", здесь тип данных определяется по имени свойства оно всегда должно начинаться со слова data после названия контейнера или массива.
- "data" - возвращает this.htmlLink.dataset[ this.parent.name + this.propName ] тоесть данные после знака "=";
Метод setProp("newProp"):
-
setProp("newProp")- для "text","html","inputvalue", "select","checkbox", "radio", "data", 'href', 'src', "id", "style" - утанавливает переданное в него значение свойству; -
"class"- если передать название класса то добавит класс, если передать массив с классами - удалит все классы и установит новые из массива;
-
для стандартных событий - добавляет еще оди обработчик события к данному свойству
setProp("event_name", event_method); -
для пользовательских событий нечего не делает, возвращает false;
-
"render-variant" и "group" - смотреть ниже;
Метод removeProp():
removeProp()- "text","html","inputvalue", "select", "data", "style" - устанавливает данные свойства - "", можно также использовать метод setProp("");- для "checkbox", "radio", устанавливает false;
- для атрибутов ('disabled', 'href', 'src', "id",) -удаляет данный атрибут.
- "class"- если передать название класса то удалит класс, если передать массив с классами - удалит несколько классов;
- для стандартных событий - убирают обработчик события по названию
removeProp("event_name") - для пользовательских событий нечего не делает, возвращает false;
Для свойств с типом "group" и "render-variant" - смотреть ниже;
Для свойст являющихся стандартными событиями есть два дополнительных метода:
-
this.disableEvent(eventName)- временно отключить событие eventName на данном свойстве; -
this.enableEvent(eventName)- включает отключенное событие; -
this.emitEvent(eventName)- вызывает метод обработчик события eventName;
Для пользовательских событий:
-
this.disableEvent()- временно отключить прослушивание пользовательского события на данном свойстве; -
this.enableEvent()- включает отключенное событие;
Виртуальный массив это массив в котором нет ссылки на html тег. Он нужен для создания контейнеров не сгрупированных в одном html элементе а разбросаных по разным свойсвам group разных контейнеров.
Например у нас есть три контейнера и в каждом есть свойство с типом group
<div data-pages=array>
<div data-page="container">
<div data-page-some_group="group">
</div>
</div>
<div data-page="container">
<div data-page-some_group="group">
</div>
</div>
<div data-page="container">
<div data-page-some_group="group">
</div>
</div>
</div>
var StateMap ={
pages: {
container: "page",
props: ["some_group"],
methods: {
}
}
}
Теперь в каждом свойстве "some_group" мы хотим поместить различное количество пунктов меню data-item="container" например в первом 1, во втором 2, в третьем 3 если использовать обычные массивы то нам прийдется создать три одинаковых массива с различным набором data-item="container" и одинаковой функциональностью. Чтобы этого не делать мы создадим один виртуальный массив items а его контейнеры мы распределим между свойствами "some_group"
Итак html код будет выглядеть так:
<div data-pages=array>
<div data-page="container">
<div data-page-some_group="group">
<div data-item="container" data-item-text="text">текст 1</div> <!-- добавили контейнер item -->
</div>
</div>
<div data-page="container">
<div data-page-some_group="group">
<div data-item="container" data-item-text="text">текст 1</div>
<div data-item="container" data-item-text="text">текст 2</div>
</div>
</div>
<div data-page="container">
<div data-page-some_group="group">
<div data-item="container" data-item-text="text">текст 1</div>
<div data-item="container" data-item-text="text">текст 2</div>
<div data-item="container" data-item-text="text">текст 3</div>
</div>
</div>
</div>
Теперь добавим виртуальный массив items в javascript код:
var StateMap ={
pages: {
container: "page",
props: ["some_group"],
methods: {
}
},
virtualArrayComponents: { //объект для хранения виртуальных массивов
items: { //виртуальный массив
container: "item", //контейнер виртуального массива
props: ["text"],
methods: {
}
}
}
}
Теперь открыв в консоли экземпляр приложения можно увидеть что всего в массиве state.items.data у нас 6 контейнеров "item", а в свойстве some_group.groupChild первого контейнера "page" - один, второго - два, третьего - три.
Таким образом с помощью свойства с типом "group" мы сгрупировали в трех контейнрах "page" массива "pages" различное количество контейнеров "item" из массива 'items'
Заметьте что index контейнера в массиве отличается от индекса контейнера в группе groupId Теперь если мы захотим удалить контейнер из группы, с помощью метода .removeFromGroup(groupID) то он также удалится из виртуального массива.
Важно размещать контейнеры в html разметке непосредственно в теге с типом group избегая каких либо промежуточных тегов,
т.к. при инициализации приложение будет искать только непосредственных потомков (this.htmlLink.children) с типом container,
И если их обернуть в другой тег, то оно их просто не найдет.
Свойство с типом group предназначено для группировки в себе части или всех контейнеров из какого либо виртуального массива. При инициализации данного свойства htmlix ищет в html разметке контейнеры объявленные внутри него и добавляет их в поле groupChild, а также добавляет ссылку на их виртуальный массив в поле groupArray. Затем в дальнейшем если понадобится добавить в группу новые контейнеры можно просто вызвать метод: setProp({container_prop1: "данные1", container_prop2: "данные2", и т.д. }) - чтобы добавить контейнер в группу, либо setProp([ {container_prop1: "данные1", container_prop2: "данные2", и т.д. }, {container_prop1: "данные1", container_prop2: "данные2", и т.д. }]) - чтобы перезаписать группу. Htmlix создаст контейнеры для группы из массива который был сохраненн в свойство groupArray. Если при инициализации в свойстве с типом group не будет контейнеров, то соответсвенно ссылка на массив не будет создана, поэтому при добавлении новых контейнеров в группу htmlix попробует отискать ссылку на groupArray в свойствах group смежных контейнеров (если они не пустые) и если не найдет выдаст ошибку. Поэтому при создании свойства с типом group в нем либо должен быть контейнер который здесь будет отображаться либо шаблон template в свойстве group нулевого контейнера. Либо при использовании метода setProp напрямую указать виртуальный массив из которого брать шаблоны:
setProp( {componentName: "имя_виртуального_массива", group: [ {container_prop1: "данные1", container_prop2: "данные2", и т.д. }, {container_prop1: "данные1", container_prop2: "данные2", и т.д. }] } );
Открыв в консоли свойство с типом group можно увидеть следующие поля:
groupArray- ссылка на виртуальный массив контейнеров данной группы;groupChild- группа контейнеров из виртуального массива, порядковый номер совпадает с полем groupId конкретного контейнера группы.
Также у свойства с типом group имеются дополнительные методы:
-
.order(newOrderArr)- изменяет порядок контейнеров в группе и html разметке newOrderArr- массив "старых" groupId в новом порядке; -
.removeFromGroup(groupID)- удаляет контейнер из группы а также из виртуального массива, где groupID - индекс контейнера в группе; -
.clearGroup()- удаляет все контейнеры из данного свойства а также из виртуального массива; -
.addToGroup(container, insertLocation)- добавляет контейнер в группу и создает в нем поля .groupId - индекс группы, .groupParent - ссылка на свойство в котором находится контейнер где container - сам контейнер, insertLocation - позиция для вставки, если не указать то вставит в конец группы; -
reuseGroup(arrayWithObjects)- аналогичен reuseAll(arrayWithObjects) - только не для массива а для группы, единственная особенность его работы заключается в том, что в свойстве должна быть ссылка на виртуальный массив данной группы this.groupArray если в данном свойстве ее нет, то он попытается отыскать ее в свойствах смежных контейнеров, если не найдет то не сработает. Поэтому при создании разметки, в свойстве с типом group нулевого контейнера, должен находиться контейнер который здесь будет отображаться, чтобы запомнить на него ссылку, если необходимо создать пустое свойство для всех контейнеров и только в процессе добавлять туда контейнеры, необходимо создать шаблон с типом "template" и (style="display: non") для того чтобы запомнить ссылку на массив группы, либо использовать метод.createNewGroup(groupArr, componentName) -
.createInGroup(props, insertLocation)- создает контейнер в виртуальном массиве и затем добавляет в группу на указанную позицию, если не указать добавит в конец, для работы требует наличие поля this.groupArray !=null которое добавляется при инициализации приложения. Поэтому при создании разметки, в свойстве с типом group нулевого контейнера, должен находиться контейнер который здесь будет отображаться, чтобы запомнить на него ссылку, если необходимо создать пустое свойство для всех контейнеров и только в процессе добавлять туда контейнеры, необходимо создать шаблон с типом "template" и (style="display: non") для того чтобы запомнить ссылку на массив группы. -
.createNewGroup(groupArr, componentName)- если виртуальный массив данной группы this.groupArray.pathToComponent совпадает с именемcomponentNameто вызывает методreuseGroup(groupArr), если не совпадает, удаляет все из группы и создает новую с контейнерами из виртуального массиваcomponentName, гдеgroupArr- массив с объектами, ключи которых - имена свойств, а данные этих ключей, данные для свойств.
Вызов метода setProp(val) - в свойстве с типом group:
- если val -объект с начальными данными для свойств в формате ключ - значение, вызовет метод
.createInGroup(val, insertLocation), insertLocation=val.location - если val - массив с объектами, ключами которых являются имена свойств - вызовет метод
reuseGroup(val) - если val объект с полями
val.componentName-имя виртуального массива,val.group- массив с объектами, ключами которых являются имена свойств - вызовет метод.createNewGroup(val.group, val.componentName)
Вызов метода removeProp(val)
- если не передать параметр вызовет
.clearGroup() - если передать вызовет
.removeFromGroup(val=groupID)
Вызов метода getProp() возвращает массив с объектами ключами которых будут названия свойств, а значения,
данные полученные из этих свойств на каждом контейнере, использует работает на основе метода getAllProps(),
если передать параметром mapObject={key1: "", key2: {ke1: ""} } .getProp(mapObject),
вернет свойства только совпадающие по названиям с key, таким образом можно получить развернутый объект со всеми значениями свойств либо со всеми нужными значениями свойств из дочерних контейнеров;
Свойства с типом "render-variant" используются для отображения в себе одних и скрытия других компонентов;
Например у нас есть компонент page с кнопкой click и сойство variant с типом "render-variant", а также еще два компонента variant1 и variant2
<div data-page="container">
<div data-page-variant="render-variant">
<div data-variant1="container" data-variant1-style="style">текст первого варианта</div>
</div>
<button data-page-click="click">сменить вариант</button>
</div>
<div data-variant2="container" data-variant2-style="style" style="display: none;">текст второго варианта<div>
var StateMap ={
page: {
container: "page",
props: ["variant", "click"],
methods: {
click: function(){
var variant = this.parent.props.variant; //получаем ссылку на свойство вариант из свойства click;
console.log(variant);
var newVariant = "variant2"; //имя нового компонента для отображения
if(variant.renderChild.pathToCоmponent == "variant2") newVariant = "variant1" //если текущий компонент для отображения "variant2" меняем его на "variant1"
variant.setProp(newVariant); //отображаем новый вариант
variant.renderChild.props.style.setProp('dysplay: "" '); //убираем display none у скрытого варианта
}
}
},
variant1: {
container: "variant1",
props: [],
methods: {
}
},
variant2: {
container: "variant2",
props: ["style"],
methods: {
}
}
}
При построении реального приложения неотображаемые варианты обычно догружаются в fetch запросе поэтому их нет надобности скрывать с помощью стилей display none, поэтому код переключения вариантов значительно меньше.
Свойство с типом render-variant предназначено для отображения и смены внутри себя различных компонентов. Это может быть массив, контейнер с renderType = "container-outer", либо контейнер из виртуального массива. При инициализации приложения htmlix добавляет ссылку на отображаемый компонент в поле renderChild. Для смены компонента достаточно вызвать метод setProp("имя-компонента"), либо setProp("ссылка-на-контейнер-виртуального-массива"). Предыдущий компонент станет невидимым, а новый отобразится в данном свойстве, если это контейнер из виртуального массива то он будет удален. Можно также не только поменять отображаемый в свойстве компонент но и установить новые значения для его свойств:
setProp({ componentName: "имя_отображаемого_компонента", prop1: "данные_свойства_1", prop2: "данные_свойства_2", и т.д.}) - если отображаемый компонент является контейнером.
setProp({ componentName: "имя_отображаемого_компонента", data: [ { prop1: "данные_свойства_1", prop2: "данные_свойства_2", и т.д.}, { prop1: "данные_свойства_1", prop2: "данные_свойства_2", и т.д.} ]) - если отображаемый компонент является массивом.
Открыв в консоли свойство с типом render-variant в нем будут следующие поля:
renderChild- ссылка на текущий компонент отображаемый в данном свойстве;
В отображаемом компоненте также добавилось поле renderParent
renderParent- ссылка на свойство в котором отображается данный компонент;
Также у свойства с типом render-variant есть несколько дополнительных методов:
.render(nameComponent=string) - отображает компонент с именем "nameComponent" (используется только для отображения компонентов)
.renderByContainer(containerLink=container) - отображает контейнер из виртуального массива по ссылке (используется только для отображения контейнеров из виртуальных массивов);
.setOrCreateAndRender(objWidthProps) - меняет отображаемый компонента на objWidthProps.componentName и устанавливает ему новые значения свойств;
-
objWidthProps - объект с новыми значениями для свойств, где имена ключей объекта должны совпадать с названиями устанавливаемых свойств, objWidthProps.componentName - обязательное поле объекта, которое содержит имя отображаемого компонента, если отображаемый компонент является контейнером из виртуального массива здесь указывается имя виртуального массива. Если отображаемый компонент является обычным массивом, здесь также нужно указать поле data - массив с объектами, ключами которых являются новые свойства для контейнеров.
-
если
objWidthProps.componentName- обычный контейнер(renderType="container-outer") - устанавливает ему свойства совпавшие с ключами в объекте objWidthProps, вызвав у него методsetAllProps(objWidthProps)а затем методrender(objWidthProps.componentName); -
если
objWidthProps.componentName- виртуальный массив(renderType="virtual-array") то удалит старый отображаемый контейнер в свойстве и создаст новый из виртуального массива вызвав метод массиваcontainer=componentName.add(objWidthProps)затем вызовет метод.renderByContainer(containerLink=container), -
если
objWidthProps.componentName- обычный массив(renderType="array"), отображает в свойстве данный массив и вызывает у него методreuseAll(objWidthProps.data), передав в него массивdataиз объектаobjWidthPropsс новыми данными для контеййнеров.
.setProp(newElement) - метод определит тип данных и затем вызовет нужный метод из перечисленных выше,
- для текста .render (переключает одиночные контейнеры и массивы newElement=text),
- для контейнера из виртуального массива .renderByContainer(newElement=container);
- для объекта, с присутствующим полем newElement.componentName будет вызван .setOrCreateAndRender(newElement)
.getProp() - если отображаемый элемент является контейнером, вызывает у него метод .getAllProps(), если отображаемый элемент является массивом, вызывает у него метод метод .getAll(),
тем самым получает значения всех свойств из отображаемого элемента, если передать параметром mapObject={key1: "", key2: {ke1: ""} } .getProp(mapObject),
вернет свойства только совпадающие по названиям с key, таким образом можно получить развернутый объект со всеми значениями свойств либо со всеми нужными значениями свойств из отображаемого элемента;
.removeProp() - убирает компонент из видимости, если это контейнер из виртуального массива (renderType == "container-inner") удаляет его;
В примере выше (#Render-variant) мы рассмотрели изменение отображаемых компонентов в свойстве с типом render-variant, шаблон для которого скрывали и загружали на той-же html странице, но что если мы не хотим писать все шаблоны на одной станице, чтобы не создавать путаницы, для этого можно "догрузить" остальные шаблоны - неиспользуемые при первой загрузке данных с сервера, как это cделать?
Давайте изменим пример выше и догрузим шаблон для компонента variant2 в fetch запросе:
<div data-page="container">
<div data-page-variant="render-variant">
<div data-variant1="container" >текст первого варианта</div>
</div>
<button data-page-click="click">сменить вариант</button>
</div> Удалили из основной разметки компонент variant2 а также свойство со стилями display: none, т.к. оно больше не понадобится. Далее создадим файл template.html и добавим в него шаблон для компонента variant2
<div data-variant2="container" data-variant2-content="text">текст второго варианта<div>
Добавили шаблон для variant2, а также удалили свойство со стилями и добавили свойство content.
Теперь изменим описани приложения:
var StateMap ={
page: {
container: "page",
props: ["variant", "click"],
methods: {
click: function(){
this.parent.props.variant.setProp("variant2"); //отображаем новый вариант
}
}
},
variant1: {
container: "variant1",
props: [],
methods: {
}
},
fetchComponents: { //поместили компонент в специальный объект fetchComponents, это говорит о том что шаблоны для него нужно искать в файле - templatePath: "./template.html" после их загрузки.
variant2: {
container: "variant2",
props: ["content"],
methods: {
}
},
}
stateSettings: {
templatePath: "./template.html"
}
}
Итак выше мы изменили описание приложения, теперь varian2 у нас помещен в объект fetchComponents что говорит о том что его нужно создать после того как догрузятся шаблоны из файла templatePath: "./template.html" который мы указали в настройках.
После клика по кнопке мы меняем отображаемый компонент в свойстве. Можно не только поменять отображаемый компонент, но и сразу установить ему какие либо свойства: .setProp({componentName: "variant2", content: "новый контент"}) передав параметром объект с именем нового компонента componentName и данными для свойств.
Рассмотрим ситуацию, когда необходимо отобразить сложный список, в каждом пункте которого, есть дополнительный вариант шаблона, например в некоторых пунктах есть параграф с дополнительным текстом, а в некоторых нет.
Давайте создадим разметку для данного списка:
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!--------------------------------->
<div data-test_array="array" class="row">
<!-- массив test_array с контейнерами списка test_container -->
<div data-test_container="container" class="card col-3">
<p data-test_container-main_text="text">контейнер 1</p>
<div data-test_container-variant="render-variant"> <!-- свойство с типом render-variant - для отображения дополнительного шаблона -->
<div data-variant_cont_1="container"> <!-- первый вариант шаблона -->
<p data-variant_cont_1-text="text" data-variant_cont_1-style="style" > текст варианта 1 </p>
</div>
</div>
</div>
<div data-test_container="container" class="card col-3">
<p data-test_container-main_text="text">контейнер 2</p>
<div data-test_container-variant="render-variant">
<div data-variant_cont_2="container"><!-- второй вариант шаблона -->
<p data-variant_cont_2-text="text" data-variant_cont_2-style="style" > текст варианта 2 </p>
<p data-variant_cont_2-text2="text">дополнительный текст</p>
</div>
</div>
</div>
<div data-test_container="container" class="card col-3">
<p data-test_container-main_text="text">контейнер 3</p>
<div data-test_container-variant="render-variant">
<div data-variant_cont_1="container"><!-- первый вариант шаблона -->
<p data-variant_cont_1-text="text" data-variant_cont_1-style="style" > текст варианта 3 </p>
</div>
</div>
</div>
</div>
<!--------------------------------->
</div>
</div>
</div>Итак выше мы создали разметку которую выдает сервер при первом запросе
Далее создадим описание приложения:
var StateMap = {
test_array: { //основной шаблон
container: "test_container",
props: ["variant", "main_text"],
methods: {
},
},
virtualArrayComponents: {
var_array_1:{
container: "variant_cont_1", //первый вариант дополнительного шаблона
props: ["text", "style"],
methods: {
}
},
var_array_2:{
container: "variant_cont_2", //второй вариант дополнительного шаблона
props: ["text", "style", "text2"],
methods: {
}
},
}
}
window.onload = function(){
var HM = new HtmlixState(StateMap);
console.log(HM);
console.log(HM.state["test_array"].getAll());
}
Итак мы создали приложение со всеми компонентами, которые затем вывели в консоль.
Тепреь к примеру мы нажали на кнопку пагинации и перешли на другую страницу, и сервер нам прислал в fetch запроссе json объект с данными для новой страници:
var resp = [
{main_text: "Название 1", variant: {componentName: "var_array_2", text: "оновной текст 1", style: "color: red;", text2: "дополнительный текст1"} },
{main_text: "название 2", variant: {componentName: "var_array_1", text: "оновной текст 2", style: "color: yellow;"} },
{main_text: "название 3", variant: {componentName: "var_array_2", text: "оновной текст 3", style: "color: red;", text2: "дополнительный текст2"} },
{main_text: "название 2", variant: {componentName: "var_array_1", text: "оновной текст 2", style: "color: yellow;"} },
];
Итак мы получили новые данные с сервера, ключами которых являются названия свойств, первое свойство основного шаблона у нас main_text с типом "text", а второе свойство это variant с типом "render-variant" в него сервер вложил объект с данными для дополнительного шаблона, а также имя дополнительного шаблона componentName,
Теперь нам необходимо обновить интерфейс на основании полученных данных, первый способ это удалить все из массива вызвав метод .removeAll() в массиве, затем в цикле добавлять новые контейнеры conainer = test_array.add(newProps), потом в каждом контейнере удалить старый вариант шаблона conainer.variant.renderChild.remove(true); затем создать новый вариант шаблона templ = rootLink.state[componentName].add(newProps); Затем добавить в контейнер новый вариант conainer.variant.renderByContainer(templ);
И второй более легкий способ это вызвать: HM.state["test_array"].reuseAll(resp);
с новыми данными сервера. Как он работает? во первых в ответе с сервера ключи объектов, должны совпадать с названиями свойств, во вторых, должно присутствовать имя отображаемого компонента
componentName для свойств с типом "render-variant", таким образом мы просто вызываем метод setProp() на каждом свойстве и передаем в него данные с ключа объекта, в ключе variant мы передаем обязательное поле componentName с именем виртуального массива,
остальные поля являются необязательными и если их не указать он возьмет их из шаблона.
Итак изменим код описания приложения:
window.onload = function(){
var HM = new HtmlixState(StateMap);
var resp = [
{main_text: "Название 1", variant: {componentName: "var_array_2", text: "оновной текст 1", style: "color: red;", text2: "дополнительный текст1"} },
{main_text: "название 2", variant: {componentName: "var_array_1", text: "оновной текст 2", style: "color: yellow;"} },
{main_text: "название 3", variant: {componentName: "var_array_2", text: "оновной текст 3", style: "color: red;", text2: "дополнительный текст2"} },
{main_text: "название 2", variant: {componentName: "var_array_1", text: "оновной текст 2", style: "color: yellow;"} },
];
window.setTimeout( function(){
HM.state["test_array"].reuseAll(resp);
console.log(HM.state["test_array"].getAll());
}, 2000);
console.log(HM);
console.log(HM.state["test_array"].getAll());
}Таким простым способом можно создавать различные ветвления, например если у каждого дополнительного варианта, будет еще какойто вариант или список с типом 'group' и т.д. приложение определит тип свойства которое мы хотим обновить и если ето "render-variant" оно сменит вариант шаблона на тот который мы указали в 'componentName' и установит ему свойства которые мы передали, а если это группа "group" то удалит все из группы и создаст новую из массива с объектами, который нужно будет разместить в значении ключа.
Так работает не только метод .reuseAll(resp), для массива, но и метод setAllProps(obgWidthProps) для контейнера и setProp(newProp) для любого свойства.
Также давайте разберем еще один метод который мы использовали выше это HM.state["test_array"].getAll() - он получает массив, с объектами ключами которых являются имена свойств,
а значениями - данные полученные вызовом метода getProp() на каждом свойстве.
Но что если мы не хотим получать все свойства например события или стили "style", для этого в него нужно передать карту - объект ключами которого, будут названия свойств которые мы хотим получить например:
console.log(HM.state["test_array"].getAll({main_text: "", variant: {text: "", text2: ""} }));
//или так:
console.log(HM.state["test_array"].getAll( {main_text: "", variant: {componentName: "", text: "", text2: ""} }) );
//componentName - название отображаемого компонента (в данном случае виртуального массива)
Все теперь свойства "style" - нет в данной выборке, таким образом мы можем получить нужные нам данные и с легкостью сохранить их на сервере, а в будущем на основании их создавать новые компоненты. Так работает не только метод массива .getAll но и метод контейнера .getAllProps и метод свойства .getProp
Тепреь давайте рассмотрим похожую ситуацию за исключением того что вместо различных вариантов шаблонов, у нас в каждом элементе будут отображаться списки из различных групп:
Начальная разметка присылаемая сервером при первой загрузке:
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!--------------------------------->
<div data-test_array="array" class="row">
<div data-test_container="container" class="card col-3">
<p data-test_container-main_text="text">контейнер 1</p>
<div data-test_container-test_group="group">
<div data-group_cont_2="container" style="border: 1px solid red">
<p data-group_cont_2-text="text" data-group_cont_2-style="style" style="color: blue;" > текст варианта 2 </p>
<p data-group_cont_2-text2="text">дополнительный текст1</p>
</div>
<div data-group_cont_2="container" style="border: 1px solid red">
<p data-group_cont_2-text="text" data-group_cont_2-style="style" style="color: blue;" > текст варианта 2 </p>
<p data-group_cont_2-text2="text">дополнительный текст2</p>
</div>
</div>
</div>
<div data-test_container="container" class="card col-3">
<p data-test_container-main_text="text">контейнер 2</p>
<div data-test_container-test_group="group">
<div data-group_cont_1="container" style="border: 1px solid green">
<p data-group_cont_1-text="text" data-group_cont_1-style="style" style="color: green;" > текст варианта 1 </p>
</div>
<div data-group_cont_1="container" style="border: 1px solid green">
<p data-group_cont_1-text="text" data-group_cont_1-style="style" style="color: green;" > текст варианта 1 </p>
</div>
</div>
</div>
</div>
<!--------------------------------->
</div>
</div>
</div>Описание приложения:
var StateMap = {
test_array: {
container: "test_container",
props: ["test_group", "main_text"],
methods: {
},
},
virtualArrayComponents: {
group_array_1:{
container: "group_cont_1",
props: ["text", "style"],
methods: {
}
},
group_array_2:{
container: "group_cont_2",
props: ["text", "style", "text2"],
methods: {
}
},
}
}
window.onload = function(){
var HM = new HtmlixState(StateMap);
console.log(HM);
}Данные с сервера которые нужно обновить:
var resp = [
{main_text: "Название 1", test_group: {componentName: "group_array_1", group:[{text: "оновной текст 2", style: "color: yellow;"}, {text: "оновной текст gg", style: "color: red;"} ] } },
{main_text: "Название 2", test_group: {componentName: "group_array_2", group:[{text: "оновной текст 3", text2: "дополнительный текст3", style: "color: red;"} , {text: "оновной текст 4", text2: "дополнительный текст4", style: "color: blue;"} ] } },
{main_text: "Название 3", test_group: {componentName: "group_array_1", group:[{text: "оновной текст 2", style: "color: yellow;"}, {text: "оновной текст gg", style: "color: red;"}, {text: "оновной текст gg", style: "color: black;"} ] } },
{main_text: "Название 4", test_group: {componentName: "group_array_2", group:[{text: "оновной текст gg", text2: "дополнительный текст2", style: "color: blue;"}, {text: "оновной текст gg", text2: "дополнительный текст5", style: "color: green;"}, {text: "оновной текст gg", text2: "дополнительный текст6", style: "color: blue;"} ] } },
];Такой же принцип как и у render-variant только теперь в каждом свойстве test_group у нас объект с двумя обязательными полями componentName - имя виртуального массива и group - массив с обьектами для каждого пункта обновляемой группы.
window.setTimeout( function(){
HM.state["test_array"].reuseAll(resp);
console.log(HM.state["test_array"].getAll());
//получение данных только необходимых свойств
console.log(HM.state["test_array"].getAll({main_text: "", test_group: {text: "", text2: ""} }));
console.log(HM.state["test_array"].getAll({main_text: "", test_group: {componentName: "", text: "", text2: ""} }));
}, 2000);Для наследования свойств контейнера другим контейнером необходимо указать поле container_extend: "имя_наследуемого_компонента". В родительском компоненте также можно указать поле share_props: numb, где numb - число свойств массива props (счет идет с начала массива), которые позволяет наследовать родительский контейнер.
Пример использования:
- Создадим html разметку двух контейнеров:
<div data-test_container="container" class="card col-3" style="color: red;">
<p data-test_container-main_text="text">контейнер первый</p>
<button data-test_container-click="click">click </button>
</div>
<div data-test_container_2="container" class="card col-3" style="color: red; margin-top: 10px;">
<p data-test_container_2-main_text="text">контейнер второй</p>
<p data-test_container_2-text2="text">click there...</p>
<button data-test_container_2-click="click">click </button>
</div>В html разметке второго контейнера, который наследует свойства первого, должны быть все наследуемые свойства первого контейнера с такими-же именами.
- Далее создадим описание приложения:
var StateMap = {
test_container: {
container: "test_container",
/// share_props - разрешает унаследовать только первые два свойства "main_text" и "click"
///если не указать то можно будет унаследовать все свойства
share_props: 2,
props: [ "main_text", "click", ["hover", "mouseover", ""] ],
methods: {
click: function(){
var text = this.parent.props.main_text.getProp();
this.parent.props.main_text.setProp(text + " 1");
},
hover: function(){ //это свойство наследоваться не будет так как его номер в массиве = 3, а мы наследуем только первые два
var text = this.parent.props.main_text.getProp();
this.parent.props.main_text.setProp(text + " 2");
}
},
},
test_container_2: {
container: "test_container_2",
///наследует свойства контейнера - компонента test_container,
//здесь указывается имя компонента из контейнера которого будут унаследованы свойства,
//если бы это был контейнер из виртуального или обычного массива, нужно указать имя массива
container_extend: "test_container",
///и добавляет два своих "text2", ['click2'
props: ["text2", ['click2', "click", "[data-test_container_2-text2='text']"] ],
methods: {
click2: function(){
var text = this.parent.props.main_text.getProp(); ///обращаемся в новом методе к свойству main_text которое унаследовали от контейнера test_container
this.parent.props.main_text.setProp(text + " 2");
}
},
},
}Итак в примере выше мы унаследовали два первых свойства: "main_text" и "click" из контейнера test_container и добавили два новых: "text2" и'click2.
Ограничения по наследованию
При создании цепочки наследований наследуемые компоненты в описании приложения должны располагаться в той последовательности в которой они наследуют свойства. Например если у нас три контейнера и мы переместим 3-й контейнер который наследует свойства 2-го из данного списка вверх на позицию 1, он не унаследует свойства первого контейнера т.к. инициализируется раньше второго в котором еще нет свойств из контейнера 1.
При использовании fetchComponents наследование свойств от уже унаследованных компонентов, может вести себя непредсказуемо, поэтому лучше загружать недостающие шаблоны с помощью stateSettings:{ templateVar: templ,}
В контейнере сначала создаются унаследованные свойства, а затем собственные свойства, это необходимо учитывать при объявлении share_props
Методы экземпляра приложения можно вызвать из любой точки this.rootLink.nameMethod();
Почти все методы экземпляра приложения являются служебными, для инициализации приложения и использования другими htmlix компонентами.
Методы общего пользования:
.getDifrentFilds(array, fild) - получает разные поля из какого либо массива array (не путать с HTMLixArray) - если массив содержит объекты, то можно указать поле объекта fild, таким образом можно "отсеять" повторяющиеся поля и получить только разные.
-
.onLoadAllметод который вызывается (при его наличии, в объектеstateMethods) после "дозагрузки" и создания всех компонентов в fetch запросе, this - в нем указывает на корневой элемент rootLink. Если компоненты создаются синхронно (без fetchComponents) данный метод вызван не будет. -
.onCreatedContainer- вызывается сразу после создания контейнера, добавляется в объектmethods- контейнера в описании приложения, this - в нем будет указывать на данный контейнер.
В Htmlix можно использовать роутер для обновления истории, а также смены отображаемых компонентов в зависимости от переданного url.
Роутер создается в экжемпляре приложения, в поле .router, для этого нужно использовать функцию HtmlixRouter(StateMap, routes) которя возвращает
экземпляр HtmlixState с новым полем .router. Первый параметр StateMap - объект описания приложения, routes - объект с картой роутов, ключами которого
являются маршруты с которыми в будущем будет сравниваться url при изменении адреса.
Как работает router? Перед инициализацией приложения и созданием всех компонентов, роутер "смотрит" url броузера, и сравнивет его с ключами из параметра routes
затем найдя соответствие, добавляет все компоненты которых нет в поле 'first' (за исключением виртуальных массивов) в объект fetchComponents (см. выше #fetchComponents),
таким образом он перестраивает описание приложения в зависимости от текущего url, те компоненты (за исключением виртуальных массивов) для которых шаблоны отдаются на данном url,
мы указываем в поле first - они создадутся сразу после загрузки страници, а те компоненты для которых нет шаблонов на данном адресе, будут добавлены автоматически в объект fetchComponents - роутером,
они создадутся сразу после дозагрузки шаблонов из файла templatePath: "/static/templates/index.html". (Для синхронного создания компонентов можно использовать templateVar см. ниже # Загрузка шаблонов из .js файла - templateVar).
Затем уже в ходе работы приложения мы используем функцию роутера this.rootLink.router.setRout(url) в которой мы меняем url, роутер сравнит новый url с ключами из параметра routes
и найдя совпадение поменяет все компоненты из поля routComponent, на те которые должны быть на данном url. В Html разметке, теги где будут меняться отображение компонентов отмечаются
data-router_carts=router, data-router_main="router" и т.д.
Например:
var routes = {
["/"]: {
first: ["categories", 'carts', "menu", "home_page"],
/// компоненты которые есть в html файле указываются в этом массиве, остальные будут загружены с шаблона, в fetch запросе асинхронно
routComponent: {
router_carts: "carts", //компоненты соответствующие данному роуту
router_main: "home_page"
},
templatePath: "/static/templates/index.html" // папка для загрузки шаблонов
},
["/cart/:idCart"]: { //знак : в начале слова - говорит что это параметр и сравенение не требуется, проверяет только его наличие на данной позиции
first: ["categories", 'cart_single', "menu", "home_page"],
routComponent: {
router_carts: "cart_single",
router_main: "home_page",
},
templatePath: "/static/templates/index.html"
},
["/category/:idCategory"]: {
first: ["categories", 'carts', "menu", "home_page"],
routComponent: {
router_carts: "carts",
router_main: "home_page"
},
templatePath: "/static/templates/index.html"
},
["/create/category"]: {
first: ["menu", "create_category"],
routComponent:{
router_main: "create_category"
},
templatePath: "/static/templates/index.html"
},
["/create/cart/"]: {
first: ["menu", "create_cart"],
routComponent:{
router_main: "create_cart"
},
templatePath: "/static/templates/index.html"
},
}
Выше приведен фрагмент кода из прототип SPA интернет магазина
В нем каждый ключ это один из возможных url для данного приложения. Знак : в начале слова говорит что эта чать url - является параметром и с ней сравнения не требуется, требуется только ее присутствие на данной позиции.
Также есть знак * в конце слова, гворит только сравнивать все что до звездочки, далее игнорируется например если у нас есть несколько похожих адресов /category1/json/ , /category2/json/ и т.д. мы указываем в ключе /category*/json/ и создаем один объект для описания роута, чтобы не дублировать код.
Знак * после слеша /* говорит не учитывать все что после слеша, то есть если роут будет ["/category/:categId/"] то он может совпать только с /category/cat_name/, но не /category/cat_name/other/, а если поставить звездочку после слеша то совпадет, так как будет проверяться только наличие, но не количество
Далее:
first- имена компонентов, которые есть в html разметке на данном адресе сервера, чтобы взять из них шаблоны. Используется при первой загрузке приложения.routComponen- объект с названиями элементов в которых переключаются компоненты на данном url, например:
routComponent: { //используется в методе `.setRout(historyUrl)`
router_carts: "carts", //найдет div элемент в котором есть data-router_carts="router" и на данном historyUrl вставит в него компонент carts
router_main: "home_page" // аналогично, найдет data-router_main="router" и заменит все что в нем есть на "home_page"
}, Таким образом вызвав из любой точки this.rootLink.router.setRout(historyUrl) - мы не только изменим историю в броузере но и поменяем компоненты отображаемые на данном url в объекте routComponent,
метод сравнит переданный historyUrl с картой ключей объекта routes и найдя совпадение поменяет отображаемые компоненты в соответствующих элементах страници.
Если не найдет совпадение выдаст в консоли ошибку что не можен найти url.
templatePath- путь к файлу с шаблонами для компонентов, шаблонов которых нет на данном адресе url. Используется при первой загрузке приложения.
Первый способ подключения недостающих шаблонов это использовать fetchComponents при данном способе сначала создаются те компоненты которые есть в разметке, отданной сервером, а остальные асинхронно, после дозагрузки шаблонов.
Второй способ это поместить шаблоны в .js файл например template.js, подключить его в index.html и указать в настройках:
файл - index.html :
<!--------------------->
<script src="./template.js"></script>Настройки StateMap:
stateSettings :{
templateVar: template, ///название переменной из файла template.js
} Сам файл template.js :
var template = `
<!--- контейнер variant_cont_2 ------------>
<div data-variant_cont_2="container" style="border: 1px solid green;">
<p data-variant_cont_2-text="text" data-variant_cont_2-style="style" style="color: green;" > текст варианта 2 </p>
<p data-variant_cont_2-text2="text" > дополнительный текст варианта 2 </p>
</div>
<!--- контейнер variant_cont_3 из виртуального массива variant_arr_3 ------------>
<div data-variant_cont_3="container" style="border: 1px solid green;">
<p data-variant_cont_3-text="text" data-variant_cont_3-style="style" style="color: green;" > текст варианта 3 </p>
</div>
`;Теперь все компоненты будут созданы синхронно.
Если в приложении используется роутер то поля first и templatePath можно не указывать, т. к. они не будут использованы, например:
var routes = {
["/"]: {
routComponent: {
router_main: "home", //компоненты соответствующие данному роуту
},
},
["/"+SITE_NAME+"/"]: {
routComponent: {
router_main: "home", //компоненты соответствующие данному роуту
},
}
Вспомогательные методы контейнера или массива объявляются в описании приложения в массиве props ---- props: [name_method, 'aux'].
В уже созданном компоненте в объекте props их не будет, они будут доступны в объекте methods контейнера или массива.
this - в методе указывает на контейнер или массив. Наследуются по тому-же принципу как и обычные свойства.
Пример использования:
container:
form: {
container: "form",
props: ["input", "click", ["test_and_send_name", "aux"] ], ///добавили свойство-вспомогательный метод с типом aux
methods: {
click: function(){
event.preventDefault();
var text = this.parent.props.input.getProp();
this.parent.methods.test_and_send_name(text); // вспомогательные (aux) методы находятся в объекте methods контейнера
console.log(text);
},
test_and_send_name: function(name){ // метод может принимать параметры
///this в методе указывает на контейнер
if(name.length < 2){
alert("имя должно быть больше двух символов");
return;
}
this.rootLink.eventProps["emiter-set-name"].setEventProp(name); // вызвали событие "emiter-set-name" из метода
window.localStorage.setItem('user_name', name);
}
},
},array:
users_array: {
selector: "div.row",
arrayProps: [ "message",
["send_message", "aux"], //добавили вспомогательный метод
['listen_set_name', "emiter-set-name", ""],
['listen_exit_user', "emiter-exit-user", ""]
],
arrayMethods: {
send_message: function(type, mess){ //вспомогательный метод принимает два параметра
if(type == "login"){
this.props.message.setProp("новый посетитель - "+mess); //this в методе массива указывает на массив
}else if(type == "logout"){
this.props.message.setProp(mess+" - покинул сайт");
}
},
listen_set_name: function(){
this.parent.add( {user_name: this.emiter.getEventProp()} );
this.parent.methods.send_message("login", this.emiter.getEventProp()); //вызвали вспомогательный метод
},
listen_exit_user: function(){
this.parent.data.forEach(container=>{
if(container.props.user_name.getProp() == this.emiter.getEventProp()){
container.remove(true);
}
});
this.parent.methods.send_message("logout", this.emiter.getEventProp()); //вызвали вспомогательный метод
},
},
container: "user",
props: [ "user_name", ],
methods: {
},
}, Cокращения для свойств, массивов и контейнеров:
-
this.$$("emiter-name")- сокращенный доступ к пользовательским событиям -
this.$$("emiter-name").set("prop")- сокращенное название setEventProp -
this.$$("emiter-name").get("prop")- сокращенное название getEventProp -
this.$()- сокращенный доступ к корневому экземпляру приложения -
this.$(componentName)- сокращенный доступ к компоненту -
this.$props(),this.$methods()- сокращенный доступ к общим методам и свойствам приложения -
this.$methods(methodName)- сокращенный доступ к stateMethods методу -
this.$props(nameProp)- сокращенный доступ к переменным stateProperties
Только для свойств:
this.props("propName")- сокращенный доступ к свойству в общем контейнере, или массиве - для свойств массиваthis.methods("nameAuxMethod")- сокращенный доступ к вспомогательному методу в общем контейнере, или массиве - для свойств массива