diff --git a/client/html/help.hbs b/client/html/help.hbs
index 5a2fccd7..2656f18e 100644
--- a/client/html/help.hbs
+++ b/client/html/help.hbs
@@ -9,5 +9,5 @@
-->
-
{{{ this.content }}}
+
diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js
index 9ee980d0..2cc53a2a 100644
--- a/client/js/controllers/help_controller.js
+++ b/client/js/controllers/help_controller.js
@@ -18,7 +18,9 @@ class HelpController {
showHelpRoute(section) {
topNavController.activate('help');
- this.helpView.render(section);
+ this.helpView.render({
+ section: section,
+ });
}
}
diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js
index 1384e15c..32144e29 100644
--- a/client/js/controllers/home_controller.js
+++ b/client/js/controllers/home_controller.js
@@ -16,7 +16,7 @@ class HomeController {
indexRoute() {
topNavController.activate('home');
- this.homeView.render();
+ this.homeView.render({});
}
notFoundRoute() {
diff --git a/client/js/controllers/top_nav_controller.js b/client/js/controllers/top_nav_controller.js
index 9ee7f07a..4e273953 100644
--- a/client/js/controllers/top_nav_controller.js
+++ b/client/js/controllers/top_nav_controller.js
@@ -32,17 +32,16 @@ class TopNavController {
'help': new NavigationItem('E', 'Help', '/help'),
};
- events.listen(
- events.Authentication,
- () => {
- this.updateVisibility();
- this.topNavView.render(this.items, this.activeItem);
- this.topNavView.activate(this.activeItem);
- });
+ const rerender = () => {
+ this.updateVisibility();
+ this.topNavView.render({
+ items: this.items,
+ activeItem: this.activeItem});
+ this.topNavView.activate(this.activeItem);
+ };
- this.updateVisibility();
- this.topNavView.render(this.items, this.activeItem);
- this.topNavView.activate(this.activeItem);
+ events.listen(events.Authentication, rerender);
+ rerender();
}
updateVisibility() {
diff --git a/client/js/controllers/users_controller.js b/client/js/controllers/users_controller.js
index eb89b028..8b84d18c 100644
--- a/client/js/controllers/users_controller.js
+++ b/client/js/controllers/users_controller.js
@@ -54,7 +54,7 @@ class UsersController {
this.user = response.user;
next();
}).catch(response => {
- this.userView.empty();
+ this.userView.emptyView(this.userView.contentHolder);
events.notify(events.Error, response.description);
});
}
diff --git a/client/js/views/base_view.js b/client/js/views/base_view.js
index bdeef9d4..98ae4067 100644
--- a/client/js/views/base_view.js
+++ b/client/js/views/base_view.js
@@ -28,16 +28,27 @@ 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.log('Missing template: ' + templatePath);
+ console.error('Missing template: ' + templatePath);
return null;
}
- const templateText = templateElement.innerHTML;
- return handlebars.compile(templateText);
+ const templateText = templateElement.innerHTML.trim();
+ const templateFactory = handlebars.compile(templateText);
+ return (...args) => {
+ return this.htmlToDom(templateFactory(...args));
+ };
}
clearMessages() {
@@ -72,12 +83,32 @@ class BaseView {
}
}
- empty() {
- this.showView('');
+ emptyView(target) {
+ return this.showView(
+ target,
+ this.htmlToDom(''));
}
- showView(html) {
- this.contentHolder.innerHTML = html;
+ 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);
+ }
+ });
}
}
diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js
index 52adfd0f..d3f198cb 100644
--- a/client/js/views/help_view.js
+++ b/client/js/views/help_view.js
@@ -15,29 +15,32 @@ class HelpView extends BaseView {
}
}
- render(section) {
- if (!section) {
- section = 'about';
- }
- if (!(section in this.sectionTemplates)) {
- this.showView('');
+ render(ctx) {
+ const target = this.contentHolder;
+ const source = this.template();
+
+ ctx.section = ctx.section || 'about';
+ if (!(ctx.section in this.sectionTemplates)) {
+ this.emptyView(this.contentHolder);
return;
}
- const content = this.sectionTemplates[section]({
- name: config.name,
- });
+ this.showView(
+ source.querySelector('.content'),
+ this.sectionTemplates[ctx.section]({
+ name: config.name,
+ }));
- this.showView(this.template({'content': content}));
-
- const allItemsSelector = '#content-holder [data-name]';
- for (let item of document.querySelectorAll(allItemsSelector)) {
- if (item.getAttribute('data-name') === section) {
+ const allItemsSelector = '[data-name]';
+ for (let item of source.querySelectorAll(allItemsSelector)) {
+ if (item.getAttribute('data-name') === ctx.section) {
item.className = 'active';
} else {
item.className = '';
}
}
+
+ this.showView(target, source);
}
}
diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js
index 9a8603d2..341982a0 100644
--- a/client/js/views/home_view.js
+++ b/client/js/views/home_view.js
@@ -9,12 +9,14 @@ class HomeView extends BaseView {
this.template = this.getTemplate('home-template');
}
- render(section) {
- this.showView(this.template({
+ render(ctx) {
+ const target = this.contentHolder;
+ const source = this.template({
name: config.name,
version: config.meta.version,
buildDate: config.meta.buildDate,
- }));
+ });
+ this.showView(target, source);
}
}
diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js
index 1d963e53..e7574160 100644
--- a/client/js/views/login_view.js
+++ b/client/js/views/login_view.js
@@ -10,14 +10,16 @@ class LoginView extends BaseView {
this.template = this.getTemplate('login-template');
}
- render(options) {
- this.showView(this.template());
- const form = this.contentHolder.querySelector('form');
- this.decorateValidator(form);
+ render(ctx) {
+ const target = this.contentHolder;
+ const source = this.template();
- const userNameField = document.getElementById('user-name');
- const passwordField = document.getElementById('user-password');
- const rememberUserField = document.getElementById('remember-user');
+ const form = source.querySelector('form');
+ const userNameField = source.querySelector('#user-name');
+ const passwordField = source.querySelector('#user-password');
+ const rememberUserField = source.querySelector('#remember-user');
+
+ this.decorateValidator(form);
userNameField.setAttribute('pattern', config.userNameRegex);
passwordField.setAttribute('pattern', config.passwordRegex);
@@ -25,8 +27,7 @@ class LoginView extends BaseView {
e.preventDefault();
this.clearMessages();
this.disableForm(form);
- options
- .login(
+ ctx.login(
userNameField.value,
passwordField.value,
rememberUserField.checked)
@@ -38,6 +39,8 @@ class LoginView extends BaseView {
events.notify(events.Error, errorMessage);
});
});
+
+ this.showView(target, source);
}
}
diff --git a/client/js/views/password_reset_view.js b/client/js/views/password_reset_view.js
index 0bba9c84..389b29d4 100644
--- a/client/js/views/password_reset_view.js
+++ b/client/js/views/password_reset_view.js
@@ -9,19 +9,20 @@ class PasswordResetView extends BaseView {
this.template = this.getTemplate('password-reset-template');
}
- render(options) {
- this.showView(this.template());
- const form = this.contentHolder.querySelector('form');
- this.decorateValidator(form);
+ render(ctx) {
+ const target = this.contentHolder;
+ const source = this.template();
- const userNameOrEmailField = document.getElementById('user-name');
+ const form = source.querySelector('form');
+ const userNameOrEmailField = source.querySelector('#user-name');
+
+ this.decorateValidator(form);
form.addEventListener('submit', e => {
e.preventDefault();
this.clearMessages();
this.disableForm(form);
- options
- .proceed(userNameOrEmailField.value)
+ ctx.proceed(userNameOrEmailField.value)
.then(() => {
events.notify(
events.Success,
@@ -33,6 +34,8 @@ class PasswordResetView extends BaseView {
events.notify(events.Error, errorMessage);
});
});
+
+ this.showView(target, source);
}
}
diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js
index 597ade99..77511e16 100644
--- a/client/js/views/registration_view.js
+++ b/client/js/views/registration_view.js
@@ -10,13 +10,14 @@ class RegistrationView extends BaseView {
this.template = this.getTemplate('user-registration-template');
}
- render(options) {
- this.showView(this.template());
+ render(ctx) {
+ const target = this.contentHolder;
+ const source = this.template();
- const form = this.contentHolder.querySelector('form');
- const userNameField = this.contentHolder.querySelector('#user-name');
- const passwordField = this.contentHolder.querySelector('#user-password');
- const emailField = this.contentHolder.querySelector('#user-email');
+ const form = source.querySelector('form');
+ const userNameField = source.querySelector('#user-name');
+ const passwordField = source.querySelector('#user-password');
+ const emailField = source.querySelector('#user-email');
this.decorateValidator(form);
userNameField.setAttribute('pattern', config.userNameRegex);
@@ -26,8 +27,7 @@ class RegistrationView extends BaseView {
e.preventDefault();
this.clearMessages();
this.disableForm(form);
- options
- .register(
+ ctx.register(
userNameField.value,
passwordField.value,
emailField.value)
@@ -39,6 +39,8 @@ class RegistrationView extends BaseView {
events.notify(events.Error, errorMessage);
});
});
+
+ this.showView(target, source);
}
}
diff --git a/client/js/views/top_nav_view.js b/client/js/views/top_nav_view.js
index 6864c633..7e9c2715 100644
--- a/client/js/views/top_nav_view.js
+++ b/client/js/views/top_nav_view.js
@@ -9,15 +9,19 @@ class TopNavView extends BaseView {
this.navHolder = document.getElementById('top-nav-holder');
}
- render(items) {
- this.navHolder.innerHTML = this.template({items: items});
- for (let link of this.navHolder.querySelectorAll('a')) {
+ render(ctx) {
+ const target = this.navHolder;
+ const source = this.template(ctx);
+
+ for (let link of source.querySelectorAll('a')) {
const regex = new RegExp(
'(' + link.getAttribute('accesskey') + ')', 'i');
link.innerHTML = link.textContent.replace(
regex,
'$1');
}
+
+ this.showView(this.navHolder, source);
}
activate(itemName) {
diff --git a/client/js/views/user_edit_view.js b/client/js/views/user_edit_view.js
index 6e1486cf..07d6e0d0 100644
--- a/client/js/views/user_edit_view.js
+++ b/client/js/views/user_edit_view.js
@@ -9,14 +9,15 @@ class UserEditView extends BaseView {
this.template = this.getTemplate('user-edit-template');
}
- render(options) {
- options.target.innerHTML = this.template(options);
+ render(ctx) {
+ const target = ctx.target;
+ const source = this.template(ctx);
- const form = options.target.querySelector('form');
- const rankField = options.target.querySelector('#user-rank');
- const emailField = options.target.querySelector('#user-email');
- const userNameField = options.target.querySelector('#user-name');
- const passwordField = options.target.querySelector('#user-password');
+ const form = source.querySelector('form');
+ const rankField = source.querySelector('#user-rank');
+ const emailField = source.querySelector('#user-email');
+ const userNameField = source.querySelector('#user-name');
+ const passwordField = source.querySelector('#user-password');
this.decorateValidator(form);
@@ -33,7 +34,7 @@ class UserEditView extends BaseView {
}
if (rankField) {
- rankField.value = options.user.rank;
+ rankField.value = ctx.user.rank;
}
/* TODO: avatar */
@@ -42,8 +43,7 @@ class UserEditView extends BaseView {
e.preventDefault();
this.clearMessages();
this.disableForm(form);
- options
- .edit(
+ ctx.edit(
userNameField.value,
passwordField.value,
emailField.value,
@@ -51,6 +51,8 @@ class UserEditView extends BaseView {
.then(user => { this.enableForm(form); })
.catch(() => { this.enableForm(form); });
});
+
+ this.showView(target, source);
}
}
diff --git a/client/js/views/user_summary_view.js b/client/js/views/user_summary_view.js
index 4c570462..ef3029a9 100644
--- a/client/js/views/user_summary_view.js
+++ b/client/js/views/user_summary_view.js
@@ -8,8 +8,10 @@ class UserSummaryView extends BaseView {
this.template = this.getTemplate('user-summary-template');
}
- render(options) {
- options.target.innerHTML = this.template(options);
+ render(ctx) {
+ const target = ctx.target;
+ const source = this.template(ctx);
+ this.showView(target, source);
}
}
diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js
index a9b9a180..89cd9c86 100644
--- a/client/js/views/user_view.js
+++ b/client/js/views/user_view.js
@@ -12,33 +12,30 @@ class UserView extends BaseView {
this.editView = new UserEditView();
}
- render(options) {
- let section = options.section;
- if (!section) {
- section = 'summary';
- }
+ render(ctx) {
+ const target = this.contentHolder;
+ const source = this.template(ctx);
- let view = null;
- if (section == 'edit') {
- view = this.editView;
- } else {
- view = this.summaryView;
- }
+ ctx.section = ctx.section || 'summary';
- this.showView(this.template(options));
-
- 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) {
+ for (let item of source.querySelectorAll('[data-name]')) {
+ if (item.getAttribute('data-name') === ctx.section) {
item.className = 'active';
} else {
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);
}
}