client/views: replace inheritance with composition
This commit is contained in:
parent
4821f2ac6c
commit
2e1823b708
15 changed files with 214 additions and 198 deletions
|
@ -69,3 +69,6 @@
|
|||
#user-delete form {
|
||||
width: 100%;
|
||||
}
|
||||
#user-delete form label {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ const api = require('../api.js');
|
|||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const misc = require('../util/misc.js');
|
||||
const views = require('../util/views.js');
|
||||
const topNavController = require('../controllers/top_nav_controller.js');
|
||||
const RegistrationView = require('../views/registration_view.js');
|
||||
const UserView = require('../views/user_view.js');
|
||||
|
@ -58,7 +59,7 @@ class UsersController {
|
|||
this.user = response.user;
|
||||
next();
|
||||
}).catch(response => {
|
||||
this.userView.emptyView(this.userView.contentHolder);
|
||||
views.emptyView(document.getElementById('content-holder'));
|
||||
events.notify(events.Error, response.description);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
let listeners = [];
|
||||
|
||||
function unlisten(messageClass) {
|
||||
listeners[messageClass] = [];
|
||||
}
|
||||
|
||||
function listen(messageClass, handler) {
|
||||
if (!(messageClass in listeners)) {
|
||||
listeners[messageClass] = [];
|
||||
|
@ -25,4 +29,5 @@ module.exports = {
|
|||
|
||||
notify: notify,
|
||||
listen: listen,
|
||||
unlisten: unlisten,
|
||||
};
|
||||
|
|
125
client/js/util/views.js
Normal file
125
client/js/util/views.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
'use strict';
|
||||
|
||||
require('../util/polyfill.js');
|
||||
const handlebars = require('handlebars');
|
||||
const events = require('../events.js');
|
||||
const domParser = new DOMParser();
|
||||
|
||||
function _messageHandler(target, message, className) {
|
||||
if (!message) {
|
||||
message = 'Unknown message';
|
||||
}
|
||||
const messagesHolder = target.querySelector('.messages');
|
||||
if (!messagesHolder) {
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
/* TODO: animate this */
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = message.replace(/\n/g, '<br/>');
|
||||
node.classList.add('message');
|
||||
node.classList.add(className);
|
||||
messagesHolder.appendChild(node);
|
||||
}
|
||||
|
||||
|
||||
function listenToMessages(target) {
|
||||
events.unlisten(events.Success);
|
||||
events.unlisten(events.Error);
|
||||
events.listen(
|
||||
events.Success, msg => { _messageHandler(target, msg, 'success'); });
|
||||
events.listen(
|
||||
events.Error, msg => { _messageHandler(target, msg, 'error'); });
|
||||
}
|
||||
|
||||
function clearMessages(target) {
|
||||
const messagesHolder = target.querySelector('.messages');
|
||||
/* TODO: animate that */
|
||||
while (messagesHolder.lastChild) {
|
||||
messagesHolder.removeChild(messagesHolder.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function htmlToDom(html) {
|
||||
const parsed = domParser.parseFromString(html, 'text/html').body;
|
||||
return parsed.childNodes.length > 1 ?
|
||||
parsed.childNodes :
|
||||
parsed.firstChild;
|
||||
}
|
||||
|
||||
function getTemplate(templatePath) {
|
||||
const templateElement = document.getElementById(templatePath + '-template');
|
||||
if (!templateElement) {
|
||||
console.error('Missing template: ' + templatePath);
|
||||
return null;
|
||||
}
|
||||
const templateText = templateElement.innerHTML.trim();
|
||||
const templateFactory = handlebars.compile(templateText);
|
||||
return (...args) => {
|
||||
return htmlToDom(templateFactory(...args));
|
||||
};
|
||||
}
|
||||
|
||||
function decorateValidator(form) {
|
||||
// postpone showing form fields validity until user actually tries
|
||||
// to submit it (seeing red/green form w/o doing anything breaks POLA)
|
||||
const submitButton = form.querySelector('.buttons input');
|
||||
submitButton.addEventListener('click', e => {
|
||||
form.classList.add('show-validation');
|
||||
});
|
||||
form.addEventListener('submit', e => {
|
||||
form.classList.remove('show-validation');
|
||||
});
|
||||
}
|
||||
|
||||
function disableForm(form) {
|
||||
for (let input of form.querySelectorAll('input')) {
|
||||
input.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function enableForm(form) {
|
||||
for (let input of form.querySelectorAll('input')) {
|
||||
input.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function emptyView(target) {
|
||||
const ret = showView(target, htmlToDom('<div class="messages"></div>'));
|
||||
listenToMessages(target);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function showView(target, source) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let observer = new MutationObserver(mutations => {
|
||||
resolve();
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(target, {childList: true});
|
||||
while (target.lastChild) {
|
||||
target.removeChild(target.lastChild);
|
||||
}
|
||||
if (source instanceof NodeList) {
|
||||
for (let child of source) {
|
||||
target.appendChild(child);
|
||||
}
|
||||
} else if (source instanceof Node) {
|
||||
target.appendChild(source);
|
||||
} else {
|
||||
console.error('Invalid view source', source);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
htmlToDom: htmlToDom,
|
||||
getTemplate: getTemplate,
|
||||
showView: showView,
|
||||
emptyView: emptyView,
|
||||
enableForm: enableForm,
|
||||
disableForm: disableForm,
|
||||
listenToMessages: listenToMessages,
|
||||
clearMessages: clearMessages,
|
||||
decorateValidator: decorateValidator,
|
||||
};
|
|
@ -1,115 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const handlebars = require('handlebars');
|
||||
const events = require('../events.js');
|
||||
const contentHolder = document.getElementById('content-holder');
|
||||
require('../util/polyfill.js');
|
||||
|
||||
function messageHandler(message, className) {
|
||||
if (!message) {
|
||||
message = 'Unknown message';
|
||||
}
|
||||
const messagesHolder = contentHolder.querySelector('.messages');
|
||||
if (!messagesHolder) {
|
||||
alert(message);
|
||||
return;
|
||||
}
|
||||
/* TODO: animate this */
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = message.replace(/\n/g, '<br/>');
|
||||
node.classList.add('message');
|
||||
node.classList.add(className);
|
||||
messagesHolder.appendChild(node);
|
||||
}
|
||||
|
||||
events.listen(events.Success, msg => { messageHandler(msg, 'success'); });
|
||||
events.listen(events.Error, msg => { messageHandler(msg, 'error'); });
|
||||
|
||||
class BaseView {
|
||||
constructor() {
|
||||
this.contentHolder = contentHolder;
|
||||
this.domParser = new DOMParser();
|
||||
}
|
||||
|
||||
htmlToDom(html) {
|
||||
const parsed = this.domParser.parseFromString(html, 'text/html').body;
|
||||
return parsed.childNodes.length > 1 ?
|
||||
parsed.childNodes :
|
||||
parsed.firstChild;
|
||||
}
|
||||
|
||||
getTemplate(templatePath) {
|
||||
const templateElement = document.getElementById(templatePath);
|
||||
if (!templateElement) {
|
||||
console.error('Missing template: ' + templatePath);
|
||||
return null;
|
||||
}
|
||||
const templateText = templateElement.innerHTML.trim();
|
||||
const templateFactory = handlebars.compile(templateText);
|
||||
return (...args) => {
|
||||
return this.htmlToDom(templateFactory(...args));
|
||||
};
|
||||
}
|
||||
|
||||
clearMessages() {
|
||||
const messagesHolder = this.contentHolder.querySelector('.messages');
|
||||
/* TODO: animate that */
|
||||
while (messagesHolder.lastChild) {
|
||||
messagesHolder.removeChild(messagesHolder.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
decorateValidator(form) {
|
||||
// postpone showing form fields validity until user actually tries
|
||||
// to submit it (seeing red/green form w/o doing anything breaks POLA)
|
||||
const submitButton = form.querySelector('.buttons input');
|
||||
submitButton.addEventListener('click', e => {
|
||||
form.classList.add('show-validation');
|
||||
});
|
||||
form.addEventListener('submit', e => {
|
||||
form.classList.remove('show-validation');
|
||||
});
|
||||
}
|
||||
|
||||
disableForm(form) {
|
||||
for (let input of form.querySelectorAll('input')) {
|
||||
input.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
enableForm(form) {
|
||||
for (let input of form.querySelectorAll('input')) {
|
||||
input.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
emptyView(target) {
|
||||
return this.showView(
|
||||
target,
|
||||
this.htmlToDom('<div class="messages"></div>'));
|
||||
}
|
||||
|
||||
showView(target, source) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let observer = new MutationObserver(mutations => {
|
||||
resolve();
|
||||
observer.disconnect();
|
||||
});
|
||||
observer.observe(target, {childList: true});
|
||||
while (target.lastChild) {
|
||||
target.removeChild(target.lastChild);
|
||||
}
|
||||
if (source instanceof NodeList) {
|
||||
for (let child of source) {
|
||||
target.appendChild(child);
|
||||
}
|
||||
} else if (source instanceof Node) {
|
||||
target.appendChild(source);
|
||||
} else {
|
||||
console.error('Invalid view source', source);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseView;
|
|
@ -1,31 +1,30 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class HelpView extends BaseView {
|
||||
class HelpView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('help-template');
|
||||
this.template = views.getTemplate('help');
|
||||
this.sectionTemplates = {};
|
||||
const sectionKeys = ['about', 'keyboard', 'search', 'comments', 'tos'];
|
||||
for (let section of sectionKeys) {
|
||||
const templateName = 'help-' + section + '-template';
|
||||
this.sectionTemplates[section] = this.getTemplate(templateName);
|
||||
const templateName = 'help-' + section;
|
||||
this.sectionTemplates[section] = views.getTemplate(templateName);
|
||||
}
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = this.contentHolder;
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this.template();
|
||||
|
||||
ctx.section = ctx.section || 'about';
|
||||
if (!(ctx.section in this.sectionTemplates)) {
|
||||
this.emptyView(this.contentHolder);
|
||||
views.emptyView(target);
|
||||
return;
|
||||
}
|
||||
|
||||
this.showView(
|
||||
views.showView(
|
||||
source.querySelector('.content'),
|
||||
this.sectionTemplates[ctx.section]({
|
||||
name: config.name,
|
||||
|
@ -40,7 +39,8 @@ class HelpView extends BaseView {
|
|||
}
|
||||
}
|
||||
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class HomeView extends BaseView {
|
||||
class HomeView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('home-template');
|
||||
this.template = views.getTemplate('home');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = this.contentHolder;
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this.template({
|
||||
name: config.name,
|
||||
version: config.meta.version,
|
||||
buildDate: config.meta.buildDate,
|
||||
});
|
||||
this.showView(target, source);
|
||||
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class LoginView extends BaseView {
|
||||
class LoginView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('login-template');
|
||||
this.template = views.getTemplate('login');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = this.contentHolder;
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this.template({canSendMails: config.canSendMails});
|
||||
|
||||
const form = source.querySelector('form');
|
||||
|
@ -19,22 +17,23 @@ class LoginView extends BaseView {
|
|||
const passwordField = source.querySelector('#user-password');
|
||||
const rememberUserField = source.querySelector('#remember-user');
|
||||
|
||||
this.decorateValidator(form);
|
||||
views.decorateValidator(form);
|
||||
userNameField.setAttribute('pattern', config.userNameRegex);
|
||||
passwordField.setAttribute('pattern', config.passwordRegex);
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.clearMessages();
|
||||
this.disableForm(form);
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.login(
|
||||
userNameField.value,
|
||||
passwordField.value,
|
||||
rememberUserField.checked)
|
||||
.always(() => { this.enableForm(form); });
|
||||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
const events = require('../events.js');
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class PasswordResetView extends BaseView {
|
||||
class PasswordResetView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('password-reset-template');
|
||||
this.template = views.getTemplate('password-reset');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = this.contentHolder;
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this.template();
|
||||
|
||||
const form = source.querySelector('form');
|
||||
const userNameOrEmailField = source.querySelector('#user-name');
|
||||
|
||||
this.decorateValidator(form);
|
||||
views.decorateValidator(form);
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.clearMessages();
|
||||
this.disableForm(form);
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.proceed(userNameOrEmailField.value)
|
||||
.catch(() => { this.enableForm(form); });
|
||||
.catch(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const events = require('../events.js');
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class RegistrationView extends BaseView {
|
||||
class RegistrationView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('user-registration-template');
|
||||
this.template = views.getTemplate('user-registration');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = this.contentHolder;
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this.template();
|
||||
|
||||
const form = source.querySelector('form');
|
||||
|
@ -19,22 +17,23 @@ class RegistrationView extends BaseView {
|
|||
const passwordField = source.querySelector('#user-password');
|
||||
const emailField = source.querySelector('#user-email');
|
||||
|
||||
this.decorateValidator(form);
|
||||
views.decorateValidator(form);
|
||||
userNameField.setAttribute('pattern', config.userNameRegex);
|
||||
passwordField.setAttribute('pattern', config.passwordRegex);
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.clearMessages();
|
||||
this.disableForm(form);
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.register(
|
||||
userNameField.value,
|
||||
passwordField.value,
|
||||
emailField.value)
|
||||
.always(() => { this.enableForm(form); });
|
||||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class TopNavView extends BaseView {
|
||||
class TopNavView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('top-nav-template');
|
||||
this.template = views.getTemplate('top-nav');
|
||||
this.navHolder = document.getElementById('top-nav-holder');
|
||||
}
|
||||
|
||||
|
@ -21,7 +20,7 @@ class TopNavView extends BaseView {
|
|||
'<span class="access-key" data-accesskey="$1">$1</span>');
|
||||
}
|
||||
|
||||
this.showView(this.navHolder, source);
|
||||
views.showView(this.navHolder, source);
|
||||
}
|
||||
|
||||
activate(itemName) {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class UserDeletionView extends BaseView {
|
||||
class UserDeletionView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('user-deletion-template');
|
||||
this.template = views.getTemplate('user-deletion');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
|
@ -14,16 +13,17 @@ class UserDeletionView extends BaseView {
|
|||
|
||||
const form = source.querySelector('form');
|
||||
|
||||
this.decorateValidator(form);
|
||||
views.decorateValidator(form);
|
||||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.clearMessages();
|
||||
this.disableForm(form);
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.delete();
|
||||
});
|
||||
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../config.js');
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class UserEditView extends BaseView {
|
||||
class UserEditView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('user-edit-template');
|
||||
this.template = views.getTemplate('user-edit');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
|
@ -19,7 +18,7 @@ class UserEditView extends BaseView {
|
|||
const userNameField = source.querySelector('#user-name');
|
||||
const passwordField = source.querySelector('#user-password');
|
||||
|
||||
this.decorateValidator(form);
|
||||
views.decorateValidator(form);
|
||||
|
||||
if (userNameField) {
|
||||
userNameField.setAttribute(
|
||||
|
@ -41,17 +40,18 @@ class UserEditView extends BaseView {
|
|||
|
||||
form.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
this.clearMessages();
|
||||
this.disableForm(form);
|
||||
views.clearMessages(target);
|
||||
views.disableForm(form);
|
||||
ctx.edit(
|
||||
userNameField.value,
|
||||
passwordField.value,
|
||||
emailField.value,
|
||||
rankField.value)
|
||||
.always(() => { this.enableForm(form); });
|
||||
.always(() => { views.enableForm(form); });
|
||||
});
|
||||
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
|
||||
class UserSummaryView extends BaseView {
|
||||
class UserSummaryView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('user-summary-template');
|
||||
this.template = views.getTemplate('user-summary');
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = ctx.target;
|
||||
const source = this.template(ctx);
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
const BaseView = require('./base_view.js');
|
||||
const views = require('../util/views.js');
|
||||
const UserDeletionView = require('./user_deletion_view.js');
|
||||
const UserSummaryView = require('./user_summary_view.js');
|
||||
const UserEditView = require('./user_edit_view.js');
|
||||
|
||||
class UserView extends BaseView {
|
||||
class UserView {
|
||||
constructor() {
|
||||
super();
|
||||
this.template = this.getTemplate('user-template');
|
||||
this.template = views.getTemplate('user');
|
||||
this.deletionView = new UserDeletionView();
|
||||
this.summaryView = new UserSummaryView();
|
||||
this.editView = new UserEditView();
|
||||
}
|
||||
|
||||
render(ctx) {
|
||||
const target = this.contentHolder;
|
||||
const target = document.getElementById('content-holder');
|
||||
const source = this.template(ctx);
|
||||
|
||||
ctx.section = ctx.section || 'summary';
|
||||
|
@ -39,7 +38,8 @@ class UserView extends BaseView {
|
|||
ctx.target = source.querySelector('#user-content-holder');
|
||||
view.render(ctx);
|
||||
|
||||
this.showView(target, source);
|
||||
views.listenToMessages(target);
|
||||
views.showView(target, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue