From 4e2995f9790da5f6ac28a9a8c3670134177368a6 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Thu, 13 Jan 2022 09:04:15 +0200 Subject: [PATCH] Extract business logic from component while trying to solve re-rendering issue Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- ...amespace-select-filter-model.injectable.ts | 5 +- .../namespace-select-filter-model.ts | 77 +++++++-- .../+namespaces/namespace-select-filter.tsx | 155 +++++------------- .../+namespaces/namespace-select.tsx | 2 +- 4 files changed, 111 insertions(+), 128 deletions(-) diff --git a/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts index 72411a4dc9..0834781721 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts +++ b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts @@ -20,9 +20,12 @@ */ import { NamespaceSelectFilterModel } from "./namespace-select-filter-model"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import namespaceStoreInjectable from "../namespace-store/namespace-store.injectable"; const NamespaceSelectFilterModelInjectable = getInjectable({ - instantiate: () => new NamespaceSelectFilterModel(), + instantiate: (di) => new NamespaceSelectFilterModel({ + namespaceStore: di.inject(namespaceStoreInjectable), + }), lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.ts b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.ts index 59cb025bbc..4c83e36550 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.ts +++ b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.ts @@ -18,17 +18,22 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { observable, makeObservable, action } from "mobx"; +import { observable, makeObservable, action, untracked } from "mobx"; +import type { NamespaceStore } from "../namespace-store/namespace.store"; +import type { SelectOption } from "../../select"; +import { isMac } from "../../../../common/vars"; + +interface Dependencies { + namespaceStore: NamespaceStore; +} export class NamespaceSelectFilterModel { - constructor() { + constructor(private dependencies: Dependencies) { makeObservable(this, { menuIsOpen: observable, closeMenu: action, openMenu: action, - toggleMenu: action, - isMultiSelection: observable, - setIsMultiSelection: action, + reset: action, }); } @@ -42,13 +47,65 @@ export class NamespaceSelectFilterModel { this.menuIsOpen = true; }; - toggleMenu = () => { - this.menuIsOpen = !this.menuIsOpen; + get selectedNames() { + return untracked(() => this.dependencies.namespaceStore.selectedNames); + } + + isSelected = (namespace: string | string[]) => + this.dependencies.namespaceStore.hasContext(namespace); + + selectSingle = (namespace: string) => { + this.dependencies.namespaceStore.selectSingle(namespace); }; - isMultiSelection = false; + selectAll = () => { + this.dependencies.namespaceStore.selectAll(); + }; - setIsMultiSelection = (isMultiSelection: boolean) => { - this.isMultiSelection = isMultiSelection; + onChange = ([{ value: namespace }]: SelectOption[]) => { + if (namespace) { + if (this.isMultiSelection) { + this.dependencies.namespaceStore.toggleSingle(namespace); + } else { + this.dependencies.namespaceStore.selectSingle(namespace); + } + } else { + this.dependencies.namespaceStore.selectAll(); + } + }; + + onClick = () => { + if (!this.menuIsOpen) { + this.openMenu(); + } else if (!this.isMultiSelection) { + this.closeMenu(); + } + }; + + private isMultiSelection = false; + + onKeyDown = (event: React.KeyboardEvent) => { + if (isSelectionKey(event)) { + this.isMultiSelection = true; + } + }; + + onKeyUp = (event: React.KeyboardEvent) => { + if (isSelectionKey(event)) { + this.isMultiSelection = false; + } + }; + + reset = () => { + this.isMultiSelection = false; + this.closeMenu(); }; } + +const isSelectionKey = (event: React.KeyboardEvent): boolean => { + if (isMac) { + return event.key === "Meta"; + } + + return event.key === "Control"; // windows or linux +}; diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 09fd685178..9c1e1e6b6d 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -22,155 +22,80 @@ import "./namespace-select-filter.scss"; import React from "react"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { observer } from "mobx-react"; import { components, PlaceholderProps } from "react-select"; -import { action, makeObservable, observable, reaction } from "mobx"; import { Icon } from "../icon"; import { NamespaceSelect } from "./namespace-select"; import type { NamespaceStore } from "./namespace-store/namespace.store"; import type { SelectOption, SelectProps } from "../select"; -import { isMac } from "../../../common/vars"; import { withInjectables } from "@ogre-tools/injectable-react"; -import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable"; import type { NamespaceSelectFilterModel } from "./namespace-select-filter-model/namespace-select-filter-model"; import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model/namespace-select-filter-model.injectable"; +import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable"; interface Dependencies { - model: NamespaceSelectFilterModel, - namespaceStore: NamespaceStore + model: NamespaceSelectFilterModel; } -@observer -class NonInjectedNamespaceSelectFilter extends React.Component { - - /** - * Only updated on every open - */ - private selected = observable.set(); - private didToggle = false; - - constructor(props: SelectProps & Dependencies) { - super(props); - makeObservable(this); - } - - get model() { - return this.props.model; - } - - componentDidMount() { - disposeOnUnmount(this, [ - reaction(() => this.model.menuIsOpen, newVal => { - if (newVal) { // rising edge of selection - this.selected.replace(this.props.namespaceStore.selectedNames); - this.didToggle = false; - } - }), - ]); - } - - formatOptionLabel = ({ value: namespace, label }: SelectOption) => { - if (namespace) { - const isSelected = this.props.namespaceStore.hasContext(namespace); - - return ( -
- - {namespace} - {isSelected && } -
- ); - } - - return label; - }; - - @action - onChange = ([{ value: namespace }]: SelectOption[]) => { - if (namespace) { - if (this.model.isMultiSelection) { - this.didToggle = true; - this.props.namespaceStore.toggleSingle(namespace); - } else { - this.props.namespaceStore.selectSingle(namespace); - } - } else { - this.props.namespaceStore.selectAll(); - } - }; - - private isSelectionKey(e: React.KeyboardEvent): boolean { - if (isMac) { - return e.key === "Meta"; - } - - return e.key === "Control"; // windows or linux - } - - @action - onKeyDown = (e: React.KeyboardEvent) => { - if (this.isSelectionKey(e)) { - this.model.setIsMultiSelection(true); - } - }; - - @action - onKeyUp = (e: React.KeyboardEvent) => { - if (this.isSelectionKey(e)) { - this.model.setIsMultiSelection(false); - } - - if (!this.model.isMultiSelection && this.didToggle) { - this.model.closeMenu(); - } - }; - - @action - onClick = () => { - if (!this.model.menuIsOpen) { - this.model.openMenu(); - } else if (!this.model.isMultiSelection) { - this.model.toggleMenu(); - } - }; - - reset = () => { - this.model.setIsMultiSelection(true); - this.model.closeMenu(); - }; - +class NonInjectedNamespaceSelectFilter extends React.Component< + SelectProps & Dependencies +> { render() { return ( -
+
+this.selected.has(right.value) - +this.selected.has(left.value)} + sort={(left, right) => + +this.props.model.selectedNames.has(right.value) - + +this.props.model.selectedNames.has(left.value) + } />
); } } +const formatOptionLabelFor = + (model: NamespaceSelectFilterModel) => + ({ value: namespace, label }: SelectOption) => { + if (namespace) { + const isSelected = model.isSelected(namespace); + + return ( +
+ + {namespace} + {isSelected && } +
+ ); + } + + return label; + }; + export const NamespaceSelectFilter = withInjectables( - NonInjectedNamespaceSelectFilter, + observer(NonInjectedNamespaceSelectFilter), { getProps: (di, props) => ({ model: di.inject(namespaceSelectFilterModelInjectable), - namespaceStore: di.inject(namespaceStoreInjectable), ...props, }), }, @@ -179,7 +104,7 @@ export const NamespaceSelectFilter = withInjectables( type CustomPlaceholderProps = PlaceholderProps; interface PlaceholderDependencies { - namespaceStore: NamespaceStore + namespaceStore: NamespaceStore; } const NonInjectedPlaceholder = observer( @@ -206,7 +131,6 @@ const NonInjectedPlaceholder = observer( }, ); - const Placeholder = withInjectables( NonInjectedPlaceholder, @@ -217,4 +141,3 @@ const Placeholder = withInjectables { }; render() { - const { className, showIcons, customizeOptions, components = {}, ...selectProps } = this.props; + const { className, showIcons, customizeOptions, components = {}, namespaceStore, ...selectProps } = this.props; return (