HM-79. Добавлена работа формы с ручкой, валидация формы. HM-88. Добавлены пункты меню с js. HM-76. Добавлена подсветка элементов меню (#40)

This commit is contained in:
Nikolay
2020-08-01 17:32:59 +03:00
committed by GitHub
parent 281ec56288
commit 8c1daa5771
18 changed files with 252 additions and 137 deletions

View File

@ -16,7 +16,7 @@ class LocalStorageAPI {
* Возвращает распарсенный объект из Local Storage по ключу из конструктора * Возвращает распарсенный объект из Local Storage по ключу из конструктора
*/ */
request () { request () {
const value = localStorage.getItem(this.key) || '{}'; const value = this.api.getItem(this.key) || '{}';
return JSON.parse(value); return JSON.parse(value);
} }
@ -25,14 +25,14 @@ class LocalStorageAPI {
* @param {Object} value - значение в Local Storage * @param {Object} value - значение в Local Storage
*/ */
createOrUpdate (value) { createOrUpdate (value) {
localStorage.setItem(this.key, JSON.stringify(value)); this.api.setItem(this.key, JSON.stringify(value));
} }
/** /**
* Очищает значение Local Storage по ключу из конструктора * Очищает значение Local Storage по ключу из конструктора
*/ */
remove () { remove () {
localStorage.removeItem(this.key); this.api.removeItem(this.key);
} }
} }

View File

@ -1,4 +1,5 @@
import LocalStorageAPI from './LocalStorageAPI'; import LocalStorageAPI from './LocalStorageAPI';
import {LOCAL_STORAGE_TYPE} from './consts';
const API_NAME = 'storageServiceUITokenApi'; const API_NAME = 'storageServiceUITokenApi';
@ -16,7 +17,7 @@ const API_NAME = 'storageServiceUITokenApi';
class TokenApi { class TokenApi {
constructor () { constructor () {
this.localApi = new LocalStorageAPI(API_NAME); this.localApi = new LocalStorageAPI(API_NAME);
this.sessionApi = new LocalStorageAPI(API_NAME); this.sessionApi = new LocalStorageAPI(API_NAME, LOCAL_STORAGE_TYPE.SESSION);
} }
/** /**

View File

@ -21,17 +21,7 @@
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNavDropdown"> <div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav"> <ul class="navbar-nav"></ul>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Главная</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/api">Список хранилищ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logs">Журнал</a>
</li>
</ul>
</div> </div>
</nav> </nav>
</template> </template>
@ -83,24 +73,17 @@
<template id="login-page"> <template id="login-page">
<div class="Login__page"> <div class="Login__page">
<div class="Login__page-container btn-prymary"> <div class="Login__page-container btn-prymary">
<div class="Login__logo-box"></div> <div class="Login__logo-box">
<img class="Login__logo" alt="logo">
</div>
<h3 class="Login__title">Storage service v0.01</h3> <h3 class="Login__title">Storage service v0.01</h3>
<form class="Login__form"> <form class="Login__form needs-validation" novalidate>
<div class="form-group Login__input"> <div class="Login__inputContainer"></div>
<label for="login btn-primary">Логин пользователя:</label> <div class="form-group form-check Login__check">
<input type="text" class="form-control" id="login" aria-describedby="loginError">
<small id="loginError" class="form-text text-muted"></small>
</div>
<div class="form-group Login__input">
<label for="password">Пароль пользователя:</label>
<input type="password" class="form-control" id="password" aria-describedby="passwordError">
<small id="passwordError" class="form-text text-muted"></small>
</div>
<div class="form-group form-check Login__check">
<input type="checkbox" class="form-check-input" id="check"> <input type="checkbox" class="form-check-input" id="check">
<label class="form-check-label" for="check">Оставаться в системе</label> <label class="form-check-label" for="check">Оставаться в системе</label>
</div> </div>
<button type="submit" class="btn btn-primary Login__submit">Войти</button> <button type="submit" class="btn btn-primary Login__submit">Войти</button>
</form> </form>
</div> </div>
</div> </div>
@ -108,7 +91,7 @@
<!-- Шаблон контейнера страниц--> <!-- Шаблон контейнера страниц-->
<template id="page-container"> <template id="page-container">
<div class="PageContainer"></div> <div class="PageContainer h-100"></div>
</template> </template>
<!-- Шаблон кнопки--> <!-- Шаблон кнопки-->
<template id="button"> <template id="button">
@ -117,7 +100,7 @@
<!-- Шаблон страниц --> <!-- Шаблон страниц -->
<template id="page"> <template id="page">
<div class="Page"></div> <div class="Page h-100"></div>
</template> </template>
<!-- Шаблон Modal --> <!-- Шаблон Modal -->
@ -332,7 +315,7 @@
<template id="form-control"> <template id="form-control">
<div class="mb-3"> <div class="mb-3">
<label class="form-label"></label> <label class="form-label"></label>
<div class="form-text"></div> <div class="form-text invalid-feedback"></div>
</div> </div>
</template> </template>

View File

@ -5,16 +5,15 @@ import 'bootstrap';
import './services/AdminConfigsService'; import './services/AdminConfigsService';
import ApiPage from './components/api-page'; import ApiPage from './components/api-page';
import MainPage from './components/main-page'; import MainPage from './components/main-page';
import navMenuButtons from './components/navigation-buttons-component/NavButtonComponent'; import mainMenu from './components/main-menu/MainMenu';
import {NAV_MENU} from './components/navigation-buttons-component/constants';
import routeService from './services/RouteService'; import routeService from './services/RouteService';
import RouterPagesContainer from './components/router-pages-container/index'; import RouterPagesContainer from './components/router-pages-container/index';
import LogsPage from './components/logs-page/index'; import LogsPage from './components/logs-page/index';
import LoginPage from './components/loginPage/LoginPage'; import LoginPage from './components/login-page';
navMenuButtons.render(NAV_MENU); mainMenu.render();
const routerPagesContainer = new RouterPagesContainer(); const routerPagesContainer = new RouterPagesContainer(mainMenu);
/** /**
* Добавление страниц в Роутинг выполняется на странице app.js * Добавление страниц в Роутинг выполняется на странице app.js

View File

@ -10,6 +10,7 @@ class FormControl extends Component {
placeholder = '', placeholder = '',
initValue = '', initValue = '',
className = '', className = '',
required = false,
} = {}) { } = {}) {
super('#form-control', parentNode); super('#form-control', parentNode);
@ -19,6 +20,10 @@ class FormControl extends Component {
tagName: this.getInputTagName(type), tagName: this.getInputTagName(type),
options: { options: {
className: `form-control ${className}`, className: `form-control ${className}`,
},
args: {
type: type === FORM_TYPES.PASSWORD ? 'password' : 'text',
...(required ? {required: ''} : {}),
} }
}); });
this.input.placeholder = placeholder; this.input.placeholder = placeholder;
@ -28,6 +33,10 @@ class FormControl extends Component {
this.label.insertAdjacentElement('afterend', this.input); this.label.insertAdjacentElement('afterend', this.input);
this.label.textContent = label; this.label.textContent = label;
this.label.setAttribute('for', id); this.label.setAttribute('for', id);
this.addEventListener(this.input, 'focus', this.clearError);
this.addEventListener(this.input, 'click', this.clearError);
this.addEventListener(this.input, 'keydown', this.clearError);
} }
disabled = (value) => { disabled = (value) => {
@ -42,6 +51,14 @@ class FormControl extends Component {
this.input.value = value; this.input.value = value;
} }
setError = (errorMessage) => {
this.errorText.textContent = errorMessage;
}
clearError = () => {
this.errorText.textContent = '';
}
getInputTagName (type) { getInputTagName (type) {
switch (type) { switch (type) {
case FORM_TYPES.TEXT: case FORM_TYPES.TEXT:

View File

@ -0,0 +1,73 @@
import Component from '../component';
import Image from '../../img/logo.svg';
import FormControl from '../form-control';
import {FORM_TYPES} from '../../consts';
class LoginForm extends Component {
constructor (parentNode) {
super('#login-page', parentNode);
this.logoBox = this.mainNode.querySelector('.Login__logo-box');
this.form = this.mainNode.querySelector('.Login__form');
this.inputContainer = this.mainNode.querySelector('.Login__inputContainer');
this.submitButton = this.mainNode.querySelector('.Login__submit');
this.checkboxSystem = this.mainNode.querySelector('.form-check-input');
this.logoImage = this.mainNode.querySelector('.Login__logo');
this.logoImage.src = Image;
this.loginControl = new FormControl(this.inputContainer, {
label: 'Логин:',
id: 'login-form-user-login',
placeholder: 'Введите логин',
required: true,
});
this.passwordControl = new FormControl(this.inputContainer, {
label: 'Пароль:',
id: 'login-form-user-password',
placeholder: 'Введите пароль',
type: FORM_TYPES.PASSWORD,
required: true,
});
this.addEventListener(this.form, 'submit', this.submit);
}
disabled = (value) => {
const elements = [
this.loginControl.input,
this.passwordControl.input,
this.submitButton,
this.checkboxSystem,
];
elements.forEach((element) => {
element.disabled = value;
});
}
validateInputs = () => {
this.form.classList.add('was-validated');
const login = this.loginControl.getValue();
const password = this.passwordControl.getValue();
if (!login) {
this.loginControl.setError('Заполните логин');
}
if (!password) {
this.passwordControl.setError('Заполните пароль');
}
return this.form.checkValidity();
}
submit = (event) => {
event.preventDefault();
if (this.validateInputs()) {
this.next('submit', {
login: this.loginControl.getValue(),
password: this.passwordControl.getValue(),
});
}
}
}
export default LoginForm;

View File

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

View File

@ -1,8 +1,4 @@
.Login__page { .Login__page {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%; height: 100%;
background-color: white; background-color: white;
display: flex; display: flex;
@ -40,4 +36,4 @@
.Login__logo-box { .Login__logo-box {
display: flex; display: flex;
align-items: center; align-items: center;
} }

View File

@ -0,0 +1,29 @@
import Component from '../component/index';
import './LoginPage.css';
import LoginForm from '../login-form';
import authServiceApi from '../../api/AuthServiceAPI';
import routeService from '../../services/RouteService';
class LoginPage extends Component {
constructor (mainNodeSelector, parentNode) {
super(mainNodeSelector, parentNode);
this.form = new LoginForm(this.mainNode);
this.addSubscribe(this.form, 'submit', ({login, password}) => {
this.form.disabled(true);
authServiceApi.auth(login, password)
.then(() => {
this.form.disabled(false);
routeService.goTo('/');
})
.catch((e) => {
// TODO: Времено используется alert, потом прикрутим систему нотификаций
// eslint-disable-next-line no-alert
alert(e?.response?.data?.message || 'Неизвестная ошибка');
this.form.disabled(false);
});
});
}
}
export default LoginPage;

View File

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

View File

@ -1,38 +0,0 @@
import Component from '../component/index';
import './LoginPage.css';
import {createElement} from '../../utils/elementUtils';
import Image from '../../img/logo.svg';
class LoginPage extends Component {
constructor () {
super('#login-page', document.body);
this.logoBox = this.mainNode.querySelector('.Login__logo-box');
this.form = this.mainNode.querySelector('.Login__form');
this.submitBtn = this.mainNode.querySelector('.Login__submit');
this.logoImg = createElement({
tagName: 'img',
parentNode: this.logoBox,
options: {
className: 'Login__logo',
},
args: {
src: Image,
alt: 'logo'
}
});
this.addEventListener(this.form, 'submit', (evt) => {
this.send(evt);
});
this.addEventListener(this.submitBtn, 'click', (evt) => {
this.send(evt);
});
}
send = (evt) => {
evt.preventDefault();
}
}
export default LoginPage;

View File

@ -7,6 +7,10 @@
margin: 5px 10px; margin: 5px 10px;
} }
.nav-item {
cursor: pointer;
}
@media (max-width: 900px){ @media (max-width: 900px){
.Buttons__container { .Buttons__container {
flex-direction: column; flex-direction: column;

View File

@ -0,0 +1,89 @@
import Component from '../component/index';
import routeService from '../../services/RouteService';
import Image from '../../img/logo.svg';
import './MainMenu.css';
import {createElement} from '../../utils/elementUtils';
import {EVENTS} from '../../consts';
export const NAV_MENU = [
{
title: 'Главная',
url: '/'
},
{
title: 'Список хранилищ',
url: '/api'
},
{
title: 'Журнал',
url: '/logs'
},
];
class MainMenu extends Component {
menuItems = [];
constructor () {
super('#main-menu', document.body);
this.buttonsContainer = this.mainNode.querySelector('.navbar-nav');
this.logoImg = document.createElement('img');
this.logoImg.src = Image;
this.logoImg.alt = 'logo';
this.logoImg.className = 'Logo mr-2';
this.logoBox = this.mainNode.querySelector('.Logo__box');
this.logoBox.appendChild(this.logoImg);
this.addSubscribe(routeService, EVENTS.ROUTE_CHANGE, (route) => {
this.menuItems.forEach(({url, link}) => {
if (route.url === url) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
});
}
render = () => {
this.menuItems = NAV_MENU.map(({url, title}) => {
const li = createElement({
tagName: 'li',
parentNode: this.buttonsContainer,
options: {
className: 'nav-item',
},
});
const link = createElement({
tagName: 'a',
parentNode: li,
options: {
className: 'nav-link',
textContent: title,
},
});
this.addEventListener(li, 'click', () => {
routeService.goTo(url);
});
return {url, link};
});
}
isHide = () => {
return this.mainNode.parentNode;
}
hideMenu = () => {
this.mainNode.remove();
}
showMenu = () => {
document.body.prepend(this.mainNode);
}
}
const navMenuButtons = new MainMenu();
export default navMenuButtons;

View File

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

View File

@ -1,33 +0,0 @@
import Component from '../component/index';
import routeService from '../../services/RouteService';
import ButtonComponent from '../button-component/ButtonComponent';
import Image from '../../img/logo.svg';
import './NavButtonComponent.css';
class NavButtonComponent extends Component {
constructor () {
super('#main-menu', document.body);
this.buttonsContainer = this.mainNode.querySelector('.Buttons__container');
this.logoImg = document.createElement('img');
this.logoImg.src = Image;
this.logoImg.alt = 'logo';
this.logoImg.className = 'Logo mr-2';
this.logoBox = this.mainNode.querySelector('.Logo__box');
this.logoBox.appendChild(this.logoImg);
}
render = (menu) => {
menu.forEach((element) => {
this.button = new ButtonComponent(this.buttonsContainer, element.title, 'btn btn-outline-primary NavButton');
this.button.subscribe('click', () => {
routeService.goTo(element.url);
});
});
}
}
const navMenuButtons = new NavButtonComponent();
export default navMenuButtons;

View File

@ -1,18 +0,0 @@
export const NAV_MENU = [
{
title: 'Главная',
url: '/'
},
{
title: 'Список хранилищ',
url: '/api'
},
{
title: 'Журнал',
url: '/logs'
},
{
title: 'Контакты',
url: '/'
},
];

View File

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

View File

@ -33,7 +33,7 @@ class RouterPagesContainer extends Component {
*/ */
url; url;
constructor () { constructor (mainMenu) {
super('#page-container', document.body); super('#page-container', document.body);
this.addSubscribe(routeService, EVENTS.ROUTE_CHANGE, ({url}) => { this.addSubscribe(routeService, EVENTS.ROUTE_CHANGE, ({url}) => {
@ -42,6 +42,13 @@ class RouterPagesContainer extends Component {
return route.url === url; return route.url === url;
}) || {}; }) || {};
// Показывает или прячет меню в зависимости от роут
if (['/login'].includes(url)) {
mainMenu.hideMenu();
} else {
mainMenu.showMenu();
}
// Рендерит новую страницу, если url изменился // Рендерит новую страницу, если url изменился
if (url !== this.currentUrl) { if (url !== this.currentUrl) {
// Удаляет предыдущую страницу // Удаляет предыдущую страницу