Clean up app.current, app.previous in JS (#2156)

- Encapsulate app.current, app.previous in PageState objects
- Reorganize Page classes to use one central base class in common

Co-authored-by: Franz Liedke <franz@develophp.org>
This commit is contained in:
Alexander Skvortsov 2020-06-19 17:41:26 -04:00 committed by GitHub
parent 17eac0fa4a
commit 1c1a3b363e
26 changed files with 95 additions and 64 deletions

View File

@ -6,7 +6,6 @@ import EditCustomFooterModal from './components/EditCustomFooterModal';
import SessionDropdown from './components/SessionDropdown'; import SessionDropdown from './components/SessionDropdown';
import HeaderPrimary from './components/HeaderPrimary'; import HeaderPrimary from './components/HeaderPrimary';
import AppearancePage from './components/AppearancePage'; import AppearancePage from './components/AppearancePage';
import Page from './components/Page';
import StatusWidget from './components/StatusWidget'; import StatusWidget from './components/StatusWidget';
import HeaderSecondary from './components/HeaderSecondary'; import HeaderSecondary from './components/HeaderSecondary';
import SettingsModal from './components/SettingsModal'; import SettingsModal from './components/SettingsModal';
@ -36,7 +35,6 @@ export default Object.assign(compat, {
'components/SessionDropdown': SessionDropdown, 'components/SessionDropdown': SessionDropdown,
'components/HeaderPrimary': HeaderPrimary, 'components/HeaderPrimary': HeaderPrimary,
'components/AppearancePage': AppearancePage, 'components/AppearancePage': AppearancePage,
'components/Page': Page,
'components/StatusWidget': StatusWidget, 'components/StatusWidget': StatusWidget,
'components/HeaderSecondary': HeaderSecondary, 'components/HeaderSecondary': HeaderSecondary,
'components/SettingsModal': SettingsModal, 'components/SettingsModal': SettingsModal,

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';
import Switch from '../../common/components/Switch'; import Switch from '../../common/components/Switch';
import EditCustomCssModal from './EditCustomCssModal'; import EditCustomCssModal from './EditCustomCssModal';

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import FieldSet from '../../common/components/FieldSet'; import FieldSet from '../../common/components/FieldSet';
import Select from '../../common/components/Select'; import Select from '../../common/components/Select';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import StatusWidget from './StatusWidget'; import StatusWidget from './StatusWidget';
export default class DashboardPage extends Page { export default class DashboardPage extends Page {

View File

@ -1,13 +1,10 @@
import Page from './Page'; import Page from '../../common/components/Page';
import LinkButton from '../../common/components/LinkButton';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';
import Dropdown from '../../common/components/Dropdown'; import Dropdown from '../../common/components/Dropdown';
import Separator from '../../common/components/Separator';
import AddExtensionModal from './AddExtensionModal'; import AddExtensionModal from './AddExtensionModal';
import LoadingModal from './LoadingModal'; import LoadingModal from './LoadingModal';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import icon from '../../common/helpers/icon'; import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
export default class ExtensionsPage extends Page { export default class ExtensionsPage extends Page {
view() { view() {

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import FieldSet from '../../common/components/FieldSet'; import FieldSet from '../../common/components/FieldSet';
import Button from '../../common/components/Button'; import Button from '../../common/components/Button';
import Alert from '../../common/components/Alert'; import Alert from '../../common/components/Alert';

View File

@ -1,32 +0,0 @@
import Component from '../../common/Component';
/**
* The `Page` component
*
* @abstract
*/
export default class Page extends Component {
init() {
app.previous = app.current;
app.current = this;
app.modal.close();
/**
* A class name to apply to the body while the route is active.
*
* @type {String}
*/
this.bodyClass = '';
}
config(isInitialized, context) {
if (isInitialized) return;
if (this.bodyClass) {
$('#app').addClass(this.bodyClass);
context.onunload = () => $('#app').removeClass(this.bodyClass);
}
}
}

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import GroupBadge from '../../common/components/GroupBadge'; import GroupBadge from '../../common/components/GroupBadge';
import EditGroupModal from './EditGroupModal'; import EditGroupModal from './EditGroupModal';
import Group from '../../common/models/Group'; import Group from '../../common/models/Group';

View File

@ -21,6 +21,7 @@ import Post from './models/Post';
import Group from './models/Group'; import Group from './models/Group';
import Notification from './models/Notification'; import Notification from './models/Notification';
import { flattenDeep } from 'lodash-es'; import { flattenDeep } from 'lodash-es';
import PageState from './states/PageState';
/** /**
* The `App` class provides a container for an application, as well as various * The `App` class provides a container for an application, as well as various
@ -115,6 +116,28 @@ export default class Application {
*/ */
requestError = null; requestError = null;
/**
* The page the app is currently on.
*
* This object holds information about the type of page we are currently
* visiting, and sometimes additional arbitrary page state that may be
* relevant to lower-level components.
*
* @type {PageState}
*/
current = new PageState(null);
/**
* The page the app was on before the current page.
*
* Once the application navigates to another page, the object previously
* assigned to this.current will be moved to this.previous, while this.current
* is re-initialized.
*
* @type {PageState}
*/
previous = new PageState(null);
data; data;
title = ''; title = '';

View File

@ -30,6 +30,7 @@ import Forum from './models/Forum';
import Component from './Component'; import Component from './Component';
import Translator from './Translator'; import Translator from './Translator';
import AlertManager from './components/AlertManager'; import AlertManager from './components/AlertManager';
import Page from './components/Page';
import Switch from './components/Switch'; import Switch from './components/Switch';
import Badge from './components/Badge'; import Badge from './components/Badge';
import LoadingIndicator from './components/LoadingIndicator'; import LoadingIndicator from './components/LoadingIndicator';
@ -94,6 +95,7 @@ export default {
Component: Component, Component: Component,
Translator: Translator, Translator: Translator,
'components/AlertManager': AlertManager, 'components/AlertManager': AlertManager,
'components/Page': Page,
'components/Switch': Switch, 'components/Switch': Switch,
'components/Badge': Badge, 'components/Badge': Badge,
'components/LoadingIndicator': LoadingIndicator, 'components/LoadingIndicator': LoadingIndicator,

View File

@ -43,8 +43,6 @@ export default class ModalManager extends Component {
this.showing = true; this.showing = true;
this.component = component; this.component = component;
if (app.current) app.current.retain = true;
m.redraw(true); m.redraw(true);
const dismissible = !!this.component.isDismissible(); const dismissible = !!this.component.isDismissible();

View File

@ -1,4 +1,5 @@
import Component from '../../common/Component'; import Component from '../Component';
import PageState from '../states/PageState';
/** /**
* The `Page` component * The `Page` component
@ -8,7 +9,7 @@ import Component from '../../common/Component';
export default class Page extends Component { export default class Page extends Component {
init() { init() {
app.previous = app.current; app.previous = app.current;
app.current = this; app.current = new PageState(this.constructor);
app.drawer.hide(); app.drawer.hide();
app.modal.close(); app.modal.close();

View File

@ -0,0 +1,33 @@
import subclassOf from '../../common/utils/subclassOf';
export default class PageState {
constructor(type, data = {}) {
this.type = type;
this.data = data;
}
/**
* Determine whether the page matches the given class and data.
*
* @param {object} type The page class to check against. Subclasses are
* accepted as well.
* @param {object} data
* @return {boolean}
*/
matches(type, data = {}) {
// Fail early when the page is of a different type
if (!subclassOf(this.type, type)) return false;
// Now that the type is known to be correct, we loop through the provided
// data to see whether it matches the data in our state.
return Object.keys(data).every((key) => this.data[key] === data[key]);
}
get(key) {
return this.data[key];
}
set(key, value) {
this.data[key] = value;
}
}

View File

@ -0,0 +1,6 @@
/**
* Check if class A is the same as or a subclass of class B.
*/
export default function subclassOf(A, B) {
return A && (A === B || A.prototype instanceof B);
}

View File

@ -160,7 +160,7 @@ export default class ForumApplication extends Application {
* @return {Boolean} * @return {Boolean}
*/ */
viewingDiscussion(discussion) { viewingDiscussion(discussion) {
return this.current instanceof DiscussionPage && this.current.discussion === discussion; return this.current.matches(DiscussionPage, { discussion });
} }
/** /**

View File

@ -23,7 +23,6 @@ import PostEdited from './components/PostEdited';
import PostStream from './components/PostStream'; import PostStream from './components/PostStream';
import ChangePasswordModal from './components/ChangePasswordModal'; import ChangePasswordModal from './components/ChangePasswordModal';
import IndexPage from './components/IndexPage'; import IndexPage from './components/IndexPage';
import Page from './components/Page';
import DiscussionRenamedNotification from './components/DiscussionRenamedNotification'; import DiscussionRenamedNotification from './components/DiscussionRenamedNotification';
import DiscussionsSearchSource from './components/DiscussionsSearchSource'; import DiscussionsSearchSource from './components/DiscussionsSearchSource';
import HeaderSecondary from './components/HeaderSecondary'; import HeaderSecondary from './components/HeaderSecondary';
@ -92,7 +91,6 @@ export default Object.assign(compat, {
'components/PostStream': PostStream, 'components/PostStream': PostStream,
'components/ChangePasswordModal': ChangePasswordModal, 'components/ChangePasswordModal': ChangePasswordModal,
'components/IndexPage': IndexPage, 'components/IndexPage': IndexPage,
'components/Page': Page,
'components/DiscussionRenamedNotification': DiscussionRenamedNotification, 'components/DiscussionRenamedNotification': DiscussionRenamedNotification,
'components/DiscussionsSearchSource': DiscussionsSearchSource, 'components/DiscussionsSearchSource': DiscussionsSearchSource,
'components/HeaderSecondary': HeaderSecondary, 'components/HeaderSecondary': HeaderSecondary,

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import DiscussionHero from './DiscussionHero'; import DiscussionHero from './DiscussionHero';
import PostStream from './PostStream'; import PostStream from './PostStream';
@ -42,7 +42,7 @@ export default class DiscussionPage extends Page {
app.pane.enable(); app.pane.enable();
app.pane.hide(); app.pane.hide();
if (app.previous instanceof DiscussionPage) { if (app.previous.matches(DiscussionPage)) {
m.redraw.strategy('diff'); m.redraw.strategy('diff');
} }
} }
@ -200,6 +200,9 @@ export default class DiscussionPage extends Page {
this.stream = new PostStream({ discussion, includedPosts }); this.stream = new PostStream({ discussion, includedPosts });
this.stream.on('positionChanged', this.positionChanged.bind(this)); this.stream.on('positionChanged', this.positionChanged.bind(this));
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true); this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
app.current.set('discussion', discussion);
app.current.set('stream', this.stream);
} }
/** /**

View File

@ -1,5 +1,5 @@
import { extend } from '../../common/extend'; import { extend } from '../../common/extend';
import Page from './Page'; import Page from '../../common/components/Page';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems'; import listItems from '../../common/helpers/listItems';
import DiscussionList from './DiscussionList'; import DiscussionList from './DiscussionList';
@ -25,15 +25,15 @@ export default class IndexPage extends Page {
// If the user is returning from a discussion page, then take note of which // If the user is returning from a discussion page, then take note of which
// discussion they have just visited. After the view is rendered, we will // discussion they have just visited. After the view is rendered, we will
// scroll down so that this discussion is in view. // scroll down so that this discussion is in view.
if (app.previous instanceof DiscussionPage) { if (app.previous.matches(DiscussionPage)) {
this.lastDiscussion = app.previous.discussion; this.lastDiscussion = app.previous.get('discussion');
} }
// If the user is coming from the discussion list, then they have either // If the user is coming from the discussion list, then they have either
// just switched one of the parameters (filter, sort, search) or they // just switched one of the parameters (filter, sort, search) or they
// probably want to refresh the results. We will clear the discussion list // probably want to refresh the results. We will clear the discussion list
// cache so that results are reloaded. // cache so that results are reloaded.
if (app.previous instanceof IndexPage) { if (app.previous.matches(IndexPage)) {
app.discussions.clear(); app.discussions.clear();
} }

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import NotificationList from './NotificationList'; import NotificationList from './NotificationList';
/** /**

View File

@ -116,6 +116,7 @@ export default class Post extends Component {
let classes = (existing || '').split(' ').concat(['Post']); let classes = (existing || '').split(' ').concat(['Post']);
const user = this.props.post.user(); const user = this.props.post.user();
const discussion = this.props.post.discussion();
if (this.loading) { if (this.loading) {
classes.push('Post--loading'); classes.push('Post--loading');
@ -125,7 +126,7 @@ export default class Post extends Component {
classes.push('Post--by-actor'); classes.push('Post--by-actor');
} }
if (user && app.current.discussion && app.current.discussion.attribute('startUserId') == user.id()) { if (user && user === discussion.user()) {
classes.push('Post--by-start-user'); classes.push('Post--by-start-user');
} }

View File

@ -57,7 +57,7 @@ export default class RenameDiscussionModal extends Modal {
.save({ title }) .save({ title })
.then(() => { .then(() => {
if (app.viewingDiscussion(this.discussion)) { if (app.viewingDiscussion(this.discussion)) {
app.current.stream.update(); app.current.get('stream').update();
} }
m.redraw(); m.redraw();
this.hide(); this.hide();

View File

@ -89,7 +89,8 @@ export default class ReplyComposer extends ComposerBody {
// If we're currently viewing the discussion which this reply was made // If we're currently viewing the discussion which this reply was made
// in, then we can update the post stream and scroll to the post. // in, then we can update the post stream and scroll to the post.
if (app.viewingDiscussion(discussion)) { if (app.viewingDiscussion(discussion)) {
app.current.stream.update().then(() => app.current.stream.goToNumber(post.number())); const stream = app.current.get('stream');
stream.update().then(() => stream.goToNumber(post.number()));
} else { } else {
// Otherwise, we'll create an alert message to inform the user that // Otherwise, we'll create an alert message to inform the user that
// their reply has been posted, containing a button which will // their reply has been posted, containing a button which will

View File

@ -1,4 +1,4 @@
import Page from './Page'; import Page from '../../common/components/Page';
import ItemList from '../../common/utils/ItemList'; import ItemList from '../../common/utils/ItemList';
import affixSidebar from '../utils/affixSidebar'; import affixSidebar from '../utils/affixSidebar';
import UserCard from './UserCard'; import UserCard from './UserCard';
@ -71,6 +71,8 @@ export default class UserPage extends Page {
show(user) { show(user) {
this.user = user; this.user = user;
app.current.set('user', user);
app.setTitle(user.displayName()); app.setTitle(user.displayName());
m.redraw(); m.redraw();

View File

@ -77,7 +77,7 @@ export default class GlobalSearchState extends SearchState {
* @return {String} * @return {String}
*/ */
getInitialSearch() { getInitialSearch() {
return app.current.constructor.providesInitialSearch && this.params().q; return app.current.type.providesInitialSearch && this.params().q;
} }
/** /**

View File

@ -178,7 +178,7 @@ export default {
app.composer.show(); app.composer.show();
if (goToLast && app.viewingDiscussion(this) && !app.composer.isFullScreen()) { if (goToLast && app.viewingDiscussion(this) && !app.composer.isFullScreen()) {
app.current.stream.goToNumber('reply'); app.current.get('stream').goToNumber('reply');
} }
deferred.resolve(component); deferred.resolve(component);

View File

@ -112,7 +112,7 @@ export default {
.delete() .delete()
.then(() => { .then(() => {
this.showDeletionAlert(user, 'success'); this.showDeletionAlert(user, 'success');
if (app.current instanceof UserPage && app.current.user === user) { if (app.current.matches(UserPage, { user })) {
app.history.back(); app.history.back();
} else { } else {
window.location.reload(); window.location.reload();