From 2e1823b7080aa0b3de986eb8f35f68837d04e023 Mon Sep 17 00:00:00 2001 From: rr- Date: Sat, 9 Apr 2016 18:54:23 +0200 Subject: [PATCH] client/views: replace inheritance with composition --- client/css/users.css | 3 + client/js/controllers/users_controller.js | 3 +- client/js/events.js | 5 + client/js/util/views.js | 125 ++++++++++++++++++++++ client/js/views/base_view.js | 115 -------------------- client/js/views/help_view.js | 20 ++-- client/js/views/home_view.js | 13 +-- client/js/views/login_view.js | 21 ++-- client/js/views/password_reset_view.js | 21 ++-- client/js/views/registration_view.js | 21 ++-- client/js/views/top_nav_view.js | 9 +- client/js/views/user_deletion_view.js | 16 +-- client/js/views/user_edit_view.js | 18 ++-- client/js/views/user_summary_view.js | 10 +- client/js/views/user_view.js | 12 +-- 15 files changed, 214 insertions(+), 198 deletions(-) create mode 100644 client/js/util/views.js delete mode 100644 client/js/views/base_view.js diff --git a/client/css/users.css b/client/css/users.css index 81bfe2e..6ba3d5f 100644 --- a/client/css/users.css +++ b/client/css/users.css @@ -69,3 +69,6 @@ #user-delete form { width: 100%; } +#user-delete form label { + padding: 0; +} diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js index 005627e..d242c1f 100644 --- a/client/js/controllers/users_controller.js +++ b/client/js/controllers/users_controller.js @@ -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); }); } diff --git a/client/js/events.js b/client/js/events.js index 8fb42d4..5c5bdb0 100644 --- a/client/js/events.js +++ b/client/js/events.js @@ -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, }; diff --git a/client/js/util/views.js b/client/js/util/views.js new file mode 100644 index 0000000..98350f8 --- /dev/null +++ b/client/js/util/views.js @@ -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, '
'); + 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('
')); + 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, +}; diff --git a/client/js/views/base_view.js b/client/js/views/base_view.js deleted file mode 100644 index 98ae406..0000000 --- a/client/js/views/base_view.js +++ /dev/null @@ -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, '
'); - 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('
')); - } - - 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; diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js index d3f198c..833e548 100644 --- a/client/js/views/help_view.js +++ b/client/js/views/help_view.js @@ -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); } } diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js index 341982a..b27fb14 100644 --- a/client/js/views/home_view.js +++ b/client/js/views/home_view.js @@ -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); } } diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js index f0e90c0..9a1763c 100644 --- a/client/js/views/login_view.js +++ b/client/js/views/login_view.js @@ -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); } } diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js index 12729bc..14f0737 100644 --- a/client/js/views/password_reset_view.js +++ b/client/js/views/password_reset_view.js @@ -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); } } diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js index 7c58ab5..55ad0a1 100644 --- a/client/js/views/registration_view.js +++ b/client/js/views/registration_view.js @@ -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); } } diff --git a/client/js/views/top_nav_view.js b/client/js/views/top_nav_view.js index 7e9c271..3210564 100644 --- a/client/js/views/top_nav_view.js +++ b/client/js/views/top_nav_view.js @@ -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 { '$1'); } - this.showView(this.navHolder, source); + views.showView(this.navHolder, source); } activate(itemName) { diff --git a/client/js/views/user_deletion_view.js b/client/js/views/user_deletion_view.js index 3861685..20c950f 100644 --- a/client/js/views/user_deletion_view.js +++ b/client/js/views/user_deletion_view.js @@ -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); } } diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js index 9d5c3ae..21ac7ba 100644 --- a/client/js/views/user_edit_view.js +++ b/client/js/views/user_edit_view.js @@ -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); } } diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js index ef3029a..9369376 100644 --- a/client/js/views/user_summary_view.js +++ b/client/js/views/user_summary_view.js @@ -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); } } diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js index cb38fc1..c547549 100644 --- a/client/js/views/user_view.js +++ b/client/js/views/user_view.js @@ -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); } }