HM-30. Добавлен класс Компонент, поправлены api, внедрен тестовый ком… (#5)

This commit is contained in:
Nikolay
2020-07-07 00:35:36 +03:00
committed by GitHub
parent 06d5ed1522
commit 6dabdd42c0
20 changed files with 580 additions and 95 deletions

BIN
src/.DS_Store vendored

Binary file not shown.

View File

@ -1,9 +1,16 @@
import {v4 as uuidv4} from 'uuid';
import StorageServiceApi from './StorageServiceAPI';
/**
* @typedef Element
* @type {object}
* @property {string} _id - уникальный id элемента
*/
/**
* Класс создания api для списков данных
* @class
* @public
*/
class StorageListApi {
/**
@ -16,33 +23,47 @@ class StorageListApi {
/**
* @private
*
* @param {Array.<Element>} list - список элементов по которым осуществялется поиск
* @param {string} _id - _id искомого элемента
*
* @returns {Promise<Object, string>}
* @returns {number} - Возвращает индекс искомого эллемента по _id
*/
async _findIndex(_id) {
const list = await this.request();
_findIndex = (list, _id) => {
return list.findIndex(item => item._id === _id);
}
/**
* @public
* @returns {Promise} - возвращает все элементы списка
* @private
*
* @param {Array<Element>} list - новый список элементов
*
* @returns {Promise<Array<Element>>} - возвращает обновленный список элементов
*/
async request() {
_updateList = async (list) => {
return await this.api.createOrUpdate(this.key, list);
}
/**
* @public
*
* @returns {Promise<Array<Element>>} - возвращает все элементы списка
*/
request = async () => {
const data = await this.api.find(this.key);
return (data && data.value) || [];
}
/**
* @public
*
* @param {string} _id - _id искомого элемента списка
*
* @returns {Promise} - возвращает элемент списка или генерит ошибку
* @returns {Promise<Element>} - возвращает элемент списка или генерит ошибку
*/
async find(_id) {
find = async (_id) => {
const list = await this.request();
const findIndex = LocalStorageListApi.findIndex(list, _id);
const findIndex = this._findIndex(list, _id);
if (findIndex === -1) {
throw new Error(`Not Found _id: ${_id}`);
}
@ -50,51 +71,58 @@ class StorageListApi {
}
/**
* @public
*
* @param {Object} data - элемент списка
*
* @returns {Promise} - Возвращает вновь созданный элемент с уникальным полем _id
* @returns {Promise<Element>} - Возвращает вновь созданный элемент с уникальным полем _id
*/
async create(data) {
create = async (data) => {
const list = await this.request();
const _id = uuidv4();
const newData = {
...data,
_id,
};
await this.api.createOrUpdate(this.key, list.concat(newData));
await this._updateList(list.concat(newData));
return newData;
}
/**
* @param {Object} data - элемент списка
* @param {string} data._id - наличие _id обязательно
* @public
*
* @returns {Promise} - Возвращает обновленный элемент списка
* @param {Element} data - элемент списка
*
* @returns {Promise<Element>} - Возвращает обновленный элемент списка
*/
async update(data) {
update = async (data) => {
const list = await this.request();
const findIndex = LocalStorageListApi.findIndex(list, data._id);
const findIndex = this._findIndex(list, data._id);
if (findIndex === -1) {
throw new Error(`Not Found _id: ${data._id}`);
}
list.splice(findIndex, 1, data);
await this.api.createOrUpdate(this.key, list);
await this._updateList(list);
return data;
}
/**
* @public
*
* @param {string} _id - _id удаляемого элемента
*
* @returns {Promise} - Возвращает _id удаленного элемента или ошибку
* @returns {Promise<string>} - Возвращает _id удаленного элемента или ошибку
*/
async remove(_id) {
remove = async (_id) => {
const list = await this.request();
const findIndex = LocalStorageListApi.findIndex(list, _id);
const findIndex = this._findIndex(list, _id);
if (findIndex === -1) {
throw new Error(`Not Found _id: ${_id}`);
}
list.splice(findIndex, 1);
await this.api.createOrUpdate(this.key, list);
await this._updateList(list);
return _id;
}
}
}
export default StorageListApi;

View File

@ -1,25 +1,64 @@
import axios from 'axios';
import {API_URL, ENDPOINT} from './consts';
/**
* @typedef Store
* @type {object}
* @property {string} key
* @property {unknown} value
*/
/**
* Класс для работы с store-service api
* @class
*/
class StorageServiceApi {
URL = `${API_URL}${ENDPOINT}`;
async request() {
/**
* @public
*
* @returns {Promise<Array<Store>>} - Возвращает список всех пар ключ-значение
*/
request = async () => {
const {data} = await axios.get(this.URL);
return data;
}
async find(key) {
const {data} = await axios.get(`${this.URL}/${key}`)
/**
* @public
*
* @param {string} key - ключ хранилища в api
*
* @returns {Promise<unknown>} - Возвращает значение по указанному ключу
*/
find = async (key) => {
const {data} = await axios.get(`${this.URL}/${key}`);
return data;
}
async createOrUpdate(key, value) {
const {data} = await axios.post(this.URL, {key, value})
/**
* @public
*
* @param {string} key - ключ хранилища в api
* @param {unknown} value - значение, которое будет хранится под указанным ключом
*
* @returns {Promise<unknown>} - возвращает вновь созданный элемент
*/
createOrUpdate = async (key, value) => {
const {data} = await axios.post(this.URL, {key, value});
return data;
}
async remove(key) {
/**
* @public
*
* @param {string} key - ключ хранилища api
*
* @returns {Promise<string>} - возвращает 'ok', если удаление было выполнено
*/
remove = async (key) => {
const {data} = await axios.delete(`${this.URL}/${key}`);
return data;
}

View File

@ -1,5 +1,15 @@
/**
* @type {string}
*/
export const API_URL = 'http://vigdorov.ru:4001';
export const ENDPOINT = '/store';
export const API_KEYS = {
/**
* @type {string}
*/
export const ENDPOINT = '/store';
/**
* @type {Object<string, string>}
*/
export const API_KEYS = {
};

View File

@ -1,11 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- Шаблон Модального окна -->
<template id="test-modal">
<div class="TestModal">
<div class="TestModal__shadow"></div>
<div class="TestModal__window"></div>
</div>
</template>
<!-- Шаблон кнопки -->
<template id="test-button">
<button type="button" class="btn btn-primary">Жми меня для теста</button>
</template>
</body>
</html>

View File

@ -1,3 +1,14 @@
import './app.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap';
import 'bootstrap';
// ! TODO: 5-14 строчки удалить, после теста компонента
import TestModal from './components/test-modal';
import TestButton from './components/test-button';
const testModal = new TestModal();
const testButton = new TestButton();
testButton.subscribe('click', () => {
testModal.show();
});

View File

@ -0,0 +1,118 @@
/**
* @typedef Listener
* @type {Object}
* @property {Node} element
* @property {string} eventName
* @property {function} listener
*/
/**
* @function EventListener
* @param {unknown[]} args - аргументы функции
*/
/**
* @typedef Events
* @type {Object<string, EventListener[]>}
*/
/**
* Класс для создания компонентов приложения. Необходим для нследования.
* @class
*/
class Component {
/**
* @private
* @type {Listener[]}
*/
_listeners;
/**
* @private
* @type {Events}
*/
_events;
/**
* @public
* @type {Node} - корневой элемент компонента
*/
mainNode;
/**
* @param {string} mainNodeSelector - селектор, с помощью которого извлекается шаблон компонента
* @param {Node} parentNode - родительский Node, в который следует положить созданный элемент
*/
constructor(mainNodeSelector, parentNode) {
/**
* @type {DocumentFragment}
*/
const content = document.querySelector(mainNodeSelector).content;
if (content.children.length > 1) {
const message = '<template> должен содержать только один элемент children';
alert(message);
throw new Error(message);
}
this.mainNode = content.firstElementChild.cloneNode(true);
parentNode.appendChild(this.mainNode);
this._listeners = [];
this._events = {};
}
/**
* Метод добавления обработчиков события на Node'ы компонента
* @public
*
* @param {Node} element - элемент, на который будет навешен обработчик
* @param {string} eventName - событие, на которое будет реагировать обработчик
* @param {function} listener - обработчик события
*/
addEventListener = (element, eventName, listener) => {
element.addEventListener(eventName, listener);
this._listeners.push({element, eventName, listener});
}
/**
* Метод подписки на события компонента
* @public
*
* @param {string} eventName - событие компонента, на которое будет реагировать обработчик
* @param {EventListener} listener - обработчик события
*/
subscribe = (eventName, listener) => {
const listeners = this._events[eventName] || [];
this._events[eventName] = [
...listeners,
listener,
];
}
/**
* Метод генерирует событие
* @public
*
* @param {string} eventName - событие, которое необходимо сгенерировать
* @param {unknown[]} args - аругемнты, который необходимо передать обработчикам события
*/
next = (eventName, ...args) => {
const listeners = this._events[eventName];
listeners.forEach(listener => {
listener(...args);
});
}
/**
* Метод уничтожения компонента. Удаляет элемент из верстки, снимает обработчики и очищает подписки
* @public
*/
destroy = () => {
this._listeners.forEach(({element, eventName, listener}) => {
element.removeEventListener(eventName, listener);
});
this.mainNode.remove();
this._listeners = [];
this._events = {};
}
}
export default Component;

View File

@ -0,0 +1,3 @@
import Component from './Component';
export default Component;

View File

@ -0,0 +1,15 @@
// ! TODO: Удалить, необходим для примера работы с компонентами
import Component from '../component';
class TestButton extends Component {
constructor() {
super('#test-button', document.body);
this.addEventListener(this.mainNode, 'click', (evt) => {
this.next('click', evt);
});
}
}
export default TestButton;

View File

@ -0,0 +1,3 @@
import TestButton from './TestButton';
export default TestButton;

View File

@ -0,0 +1,31 @@
.TestModal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.TestModal_hide {
display: none;
}
.TestModal__shadow {
position: absolute;
background-color: rgba(0, 0, 0, 0.4);
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 4;
}
.TestModal__window {
width: 300px;
height: 200px;
background-color: white;
z-index: 5;
}

View File

@ -0,0 +1,38 @@
// ! TODO: Удалить, необходим для примера работы с компонентами
import Component from '../component';
import './TestModal.css';
const MAIN = 'TestModal';
const CN = {
SHADOW: `${MAIN}__shadow`,
WINDOW: `${MAIN}__window`,
HIDE_MODAL: `${MAIN}_hide`,
};
class TestModal extends Component {
constructor() {
super('#test-modal', document.body);
this.shadow = this.mainNode.querySelector(`.${CN.SHADOW}`);
this.window = this.mainNode.querySelector(`.${CN.WINDOW}`);
this.addEventListener(this.shadow, 'click', (evt) => {
if (evt.target === this.shadow) {
this.hide();
}
});
this.hide();
}
show = () => {
this.mainNode.classList.remove(CN.HIDE_MODAL);
}
hide = () => {
this.mainNode.classList.add(CN.HIDE_MODAL);
}
}
export default TestModal;

View File

@ -0,0 +1,3 @@
import TestModal from './TestModal';
export default TestModal;