client/views: refactor rendering

This commit is contained in:
rr- 2016-04-08 10:35:38 +02:00
parent 7871c69aa3
commit 0ceaa8da42
15 changed files with 147 additions and 97 deletions

View File

@ -9,5 +9,5 @@
--></ul><!-- --></ul><!--
--></nav> --></nav>
<div class='content'>{{{ this.content }}}</div> <div class='content'></div>
</div> </div>

View File

@ -18,7 +18,9 @@ class HelpController {
showHelpRoute(section) { showHelpRoute(section) {
topNavController.activate('help'); topNavController.activate('help');
this.helpView.render(section); this.helpView.render({
section: section,
});
} }
} }

View File

@ -16,7 +16,7 @@ class HomeController {
indexRoute() { indexRoute() {
topNavController.activate('home'); topNavController.activate('home');
this.homeView.render(); this.homeView.render({});
} }
notFoundRoute() { notFoundRoute() {

View File

@ -32,17 +32,16 @@ class TopNavController {
'help': new NavigationItem('E', 'Help', '/help'), 'help': new NavigationItem('E', 'Help', '/help'),
}; };
events.listen( const rerender = () => {
events.Authentication,
() => {
this.updateVisibility(); this.updateVisibility();
this.topNavView.render(this.items, this.activeItem); this.topNavView.render({
items: this.items,
activeItem: this.activeItem});
this.topNavView.activate(this.activeItem); this.topNavView.activate(this.activeItem);
}); };
this.updateVisibility(); events.listen(events.Authentication, rerender);
this.topNavView.render(this.items, this.activeItem); rerender();
this.topNavView.activate(this.activeItem);
} }
updateVisibility() { updateVisibility() {

View File

@ -54,7 +54,7 @@ class UsersController {
this.user = response.user; this.user = response.user;
next(); next();
}).catch(response => { }).catch(response => {
this.userView.empty(); this.userView.emptyView(this.userView.contentHolder);
events.notify(events.Error, response.description); events.notify(events.Error, response.description);
}); });
} }

View File

@ -28,16 +28,27 @@ events.listen(events.Error, msg => { messageHandler(msg, 'error'); });
class BaseView { class BaseView {
constructor() { constructor() {
this.contentHolder = contentHolder; 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) { getTemplate(templatePath) {
const templateElement = document.getElementById(templatePath); const templateElement = document.getElementById(templatePath);
if (!templateElement) { if (!templateElement) {
console.log('Missing template: ' + templatePath); console.error('Missing template: ' + templatePath);
return null; return null;
} }
const templateText = templateElement.innerHTML; const templateText = templateElement.innerHTML.trim();
return handlebars.compile(templateText); const templateFactory = handlebars.compile(templateText);
return (...args) => {
return this.htmlToDom(templateFactory(...args));
};
} }
clearMessages() { clearMessages() {
@ -72,12 +83,32 @@ class BaseView {
} }
} }
empty() { emptyView(target) {
this.showView('<div class="messages"></div>'); return this.showView(
target,
this.htmlToDom('<div class="messages"></div>'));
} }
showView(html) { showView(target, source) {
this.contentHolder.innerHTML = html; 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);
}
});
} }
} }

View File

@ -15,29 +15,32 @@ class HelpView extends BaseView {
} }
} }
render(section) { render(ctx) {
if (!section) { const target = this.contentHolder;
section = 'about'; const source = this.template();
}
if (!(section in this.sectionTemplates)) { ctx.section = ctx.section || 'about';
this.showView(''); if (!(ctx.section in this.sectionTemplates)) {
this.emptyView(this.contentHolder);
return; return;
} }
const content = this.sectionTemplates[section]({ this.showView(
source.querySelector('.content'),
this.sectionTemplates[ctx.section]({
name: config.name, name: config.name,
}); }));
this.showView(this.template({'content': content})); const allItemsSelector = '[data-name]';
for (let item of source.querySelectorAll(allItemsSelector)) {
const allItemsSelector = '#content-holder [data-name]'; if (item.getAttribute('data-name') === ctx.section) {
for (let item of document.querySelectorAll(allItemsSelector)) {
if (item.getAttribute('data-name') === section) {
item.className = 'active'; item.className = 'active';
} else { } else {
item.className = ''; item.className = '';
} }
} }
this.showView(target, source);
} }
} }

View File

@ -9,12 +9,14 @@ class HomeView extends BaseView {
this.template = this.getTemplate('home-template'); this.template = this.getTemplate('home-template');
} }
render(section) { render(ctx) {
this.showView(this.template({ const target = this.contentHolder;
const source = this.template({
name: config.name, name: config.name,
version: config.meta.version, version: config.meta.version,
buildDate: config.meta.buildDate, buildDate: config.meta.buildDate,
})); });
this.showView(target, source);
} }
} }

View File

@ -10,14 +10,16 @@ class LoginView extends BaseView {
this.template = this.getTemplate('login-template'); this.template = this.getTemplate('login-template');
} }
render(options) { render(ctx) {
this.showView(this.template()); const target = this.contentHolder;
const form = this.contentHolder.querySelector('form'); const source = this.template();
this.decorateValidator(form);
const userNameField = document.getElementById('user-name'); const form = source.querySelector('form');
const passwordField = document.getElementById('user-password'); const userNameField = source.querySelector('#user-name');
const rememberUserField = document.getElementById('remember-user'); const passwordField = source.querySelector('#user-password');
const rememberUserField = source.querySelector('#remember-user');
this.decorateValidator(form);
userNameField.setAttribute('pattern', config.userNameRegex); userNameField.setAttribute('pattern', config.userNameRegex);
passwordField.setAttribute('pattern', config.passwordRegex); passwordField.setAttribute('pattern', config.passwordRegex);
@ -25,8 +27,7 @@ class LoginView extends BaseView {
e.preventDefault(); e.preventDefault();
this.clearMessages(); this.clearMessages();
this.disableForm(form); this.disableForm(form);
options ctx.login(
.login(
userNameField.value, userNameField.value,
passwordField.value, passwordField.value,
rememberUserField.checked) rememberUserField.checked)
@ -38,6 +39,8 @@ class LoginView extends BaseView {
events.notify(events.Error, errorMessage); events.notify(events.Error, errorMessage);
}); });
}); });
this.showView(target, source);
} }
} }

View File

@ -9,19 +9,20 @@ class PasswordResetView extends BaseView {
this.template = this.getTemplate('password-reset-template'); this.template = this.getTemplate('password-reset-template');
} }
render(options) { render(ctx) {
this.showView(this.template()); const target = this.contentHolder;
const form = this.contentHolder.querySelector('form'); const source = this.template();
this.decorateValidator(form);
const userNameOrEmailField = document.getElementById('user-name'); const form = source.querySelector('form');
const userNameOrEmailField = source.querySelector('#user-name');
this.decorateValidator(form);
form.addEventListener('submit', e => { form.addEventListener('submit', e => {
e.preventDefault(); e.preventDefault();
this.clearMessages(); this.clearMessages();
this.disableForm(form); this.disableForm(form);
options ctx.proceed(userNameOrEmailField.value)
.proceed(userNameOrEmailField.value)
.then(() => { .then(() => {
events.notify( events.notify(
events.Success, events.Success,
@ -33,6 +34,8 @@ class PasswordResetView extends BaseView {
events.notify(events.Error, errorMessage); events.notify(events.Error, errorMessage);
}); });
}); });
this.showView(target, source);
} }
} }

View File

@ -10,13 +10,14 @@ class RegistrationView extends BaseView {
this.template = this.getTemplate('user-registration-template'); this.template = this.getTemplate('user-registration-template');
} }
render(options) { render(ctx) {
this.showView(this.template()); const target = this.contentHolder;
const source = this.template();
const form = this.contentHolder.querySelector('form'); const form = source.querySelector('form');
const userNameField = this.contentHolder.querySelector('#user-name'); const userNameField = source.querySelector('#user-name');
const passwordField = this.contentHolder.querySelector('#user-password'); const passwordField = source.querySelector('#user-password');
const emailField = this.contentHolder.querySelector('#user-email'); const emailField = source.querySelector('#user-email');
this.decorateValidator(form); this.decorateValidator(form);
userNameField.setAttribute('pattern', config.userNameRegex); userNameField.setAttribute('pattern', config.userNameRegex);
@ -26,8 +27,7 @@ class RegistrationView extends BaseView {
e.preventDefault(); e.preventDefault();
this.clearMessages(); this.clearMessages();
this.disableForm(form); this.disableForm(form);
options ctx.register(
.register(
userNameField.value, userNameField.value,
passwordField.value, passwordField.value,
emailField.value) emailField.value)
@ -39,6 +39,8 @@ class RegistrationView extends BaseView {
events.notify(events.Error, errorMessage); events.notify(events.Error, errorMessage);
}); });
}); });
this.showView(target, source);
} }
} }

View File

@ -9,15 +9,19 @@ class TopNavView extends BaseView {
this.navHolder = document.getElementById('top-nav-holder'); this.navHolder = document.getElementById('top-nav-holder');
} }
render(items) { render(ctx) {
this.navHolder.innerHTML = this.template({items: items}); const target = this.navHolder;
for (let link of this.navHolder.querySelectorAll('a')) { const source = this.template(ctx);
for (let link of source.querySelectorAll('a')) {
const regex = new RegExp( const regex = new RegExp(
'(' + link.getAttribute('accesskey') + ')', 'i'); '(' + link.getAttribute('accesskey') + ')', 'i');
link.innerHTML = link.textContent.replace( link.innerHTML = link.textContent.replace(
regex, regex,
'<span class="access-key" data-accesskey="$1">$1</span>'); '<span class="access-key" data-accesskey="$1">$1</span>');
} }
this.showView(this.navHolder, source);
} }
activate(itemName) { activate(itemName) {

View File

@ -9,14 +9,15 @@ class UserEditView extends BaseView {
this.template = this.getTemplate('user-edit-template'); this.template = this.getTemplate('user-edit-template');
} }
render(options) { render(ctx) {
options.target.innerHTML = this.template(options); const target = ctx.target;
const source = this.template(ctx);
const form = options.target.querySelector('form'); const form = source.querySelector('form');
const rankField = options.target.querySelector('#user-rank'); const rankField = source.querySelector('#user-rank');
const emailField = options.target.querySelector('#user-email'); const emailField = source.querySelector('#user-email');
const userNameField = options.target.querySelector('#user-name'); const userNameField = source.querySelector('#user-name');
const passwordField = options.target.querySelector('#user-password'); const passwordField = source.querySelector('#user-password');
this.decorateValidator(form); this.decorateValidator(form);
@ -33,7 +34,7 @@ class UserEditView extends BaseView {
} }
if (rankField) { if (rankField) {
rankField.value = options.user.rank; rankField.value = ctx.user.rank;
} }
/* TODO: avatar */ /* TODO: avatar */
@ -42,8 +43,7 @@ class UserEditView extends BaseView {
e.preventDefault(); e.preventDefault();
this.clearMessages(); this.clearMessages();
this.disableForm(form); this.disableForm(form);
options ctx.edit(
.edit(
userNameField.value, userNameField.value,
passwordField.value, passwordField.value,
emailField.value, emailField.value,
@ -51,6 +51,8 @@ class UserEditView extends BaseView {
.then(user => { this.enableForm(form); }) .then(user => { this.enableForm(form); })
.catch(() => { this.enableForm(form); }); .catch(() => { this.enableForm(form); });
}); });
this.showView(target, source);
} }
} }

View File

@ -8,8 +8,10 @@ class UserSummaryView extends BaseView {
this.template = this.getTemplate('user-summary-template'); this.template = this.getTemplate('user-summary-template');
} }
render(options) { render(ctx) {
options.target.innerHTML = this.template(options); const target = ctx.target;
const source = this.template(ctx);
this.showView(target, source);
} }
} }

View File

@ -12,33 +12,30 @@ class UserView extends BaseView {
this.editView = new UserEditView(); this.editView = new UserEditView();
} }
render(options) { render(ctx) {
let section = options.section; const target = this.contentHolder;
if (!section) { const source = this.template(ctx);
section = 'summary';
}
let view = null; ctx.section = ctx.section || 'summary';
if (section == 'edit') {
view = this.editView;
} else {
view = this.summaryView;
}
this.showView(this.template(options)); for (let item of source.querySelectorAll('[data-name]')) {
if (item.getAttribute('data-name') === ctx.section) {
options.target = this.contentHolder.querySelector(
'#user-content-holder');
view.render(options);
const allItemsSelector = '#content-holder [data-name]';
for (let item of document.querySelectorAll(allItemsSelector)) {
if (item.getAttribute('data-name') === section) {
item.className = 'active'; item.className = 'active';
} else { } else {
item.className = ''; item.className = '';
} }
} }
let view = null;
if (ctx.section == 'edit') {
view = this.editView;
} else {
view = this.summaryView;
}
ctx.target = source.querySelector('#user-content-holder');
view.render(ctx);
this.showView(target, source);
} }
} }