HM-30. Добавлен класс Компонент, поправлены api, внедрен тестовый ком… (#5)
This commit is contained in:
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
};
|
||||
23
src/app.html
23
src/app.html
@ -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>
|
||||
13
src/app.js
13
src/app.js
@ -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();
|
||||
});
|
||||
|
||||
118
src/components/component/Component.js
Normal file
118
src/components/component/Component.js
Normal 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;
|
||||
3
src/components/component/index.js
Normal file
3
src/components/component/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import Component from './Component';
|
||||
|
||||
export default Component;
|
||||
15
src/components/test-button/TestButton.js
Normal file
15
src/components/test-button/TestButton.js
Normal 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;
|
||||
3
src/components/test-button/index.js
Normal file
3
src/components/test-button/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import TestButton from './TestButton';
|
||||
|
||||
export default TestButton;
|
||||
31
src/components/test-modal/TestModal.css
Normal file
31
src/components/test-modal/TestModal.css
Normal 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;
|
||||
}
|
||||
38
src/components/test-modal/TestModal.js
Normal file
38
src/components/test-modal/TestModal.js
Normal 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;
|
||||
3
src/components/test-modal/index.js
Normal file
3
src/components/test-modal/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import TestModal from './TestModal';
|
||||
|
||||
export default TestModal;
|
||||
Reference in New Issue
Block a user