refactor event handling to consume base class for lifecycling

This commit is contained in:
undergroundwires
2021-02-04 19:51:51 +01:00
parent 34b8822ac8
commit f1e21babbf
23 changed files with 171 additions and 195 deletions

View File

@@ -4,12 +4,12 @@ import { CategoryCollectionState } from './State/CategoryCollectionState';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { Signal } from '@/infrastructure/Events/Signal'; import { EventSource } from '@/infrastructure/Events/EventSource';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>; type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext { export class ApplicationContext implements IApplicationContext {
public readonly contextChanged = new Signal<IApplicationContextChangedEvent>(); public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
public collection: ICategoryCollection; public collection: ICategoryCollection;
public currentOs: OperatingSystem; public currentOs: OperatingSystem;

View File

@@ -1,12 +1,12 @@
import { ICategoryCollectionState } from './State/ICategoryCollectionState'; import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ISignal } from '@/infrastructure/Events/ISignal'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
export interface IApplicationContext { export interface IApplicationContext {
readonly app: IApplication; readonly app: IApplication;
readonly state: ICategoryCollectionState; readonly state: ICategoryCollectionState;
readonly contextChanged: ISignal<IApplicationContextChangedEvent>; readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
changeContext(os: OperatingSystem): void; changeContext(os: OperatingSystem): void;
} }

View File

@@ -4,13 +4,13 @@ import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection'; import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { UserScriptGenerator } from './Generation/UserScriptGenerator'; import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { Signal } from '@/infrastructure/Events/Signal'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { IApplicationCode } from './IApplicationCode'; import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator'; import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export class ApplicationCode implements IApplicationCode { export class ApplicationCode implements IApplicationCode {
public readonly changed = new Signal<ICodeChangedEvent>(); public readonly changed = new EventSource<ICodeChangedEvent>();
public current: string; public current: string;
private scriptPositions = new Map<SelectedScript, CodePosition>(); private scriptPositions = new Map<SelectedScript, CodePosition>();

View File

@@ -1,7 +1,7 @@
import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { ISignal } from '@/infrastructure/Events/ISignal'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
export interface IApplicationCode { export interface IApplicationCode {
readonly changed: ISignal<ICodeChangedEvent>; readonly changed: IEventSource<ICodeChangedEvent>;
readonly current: string; readonly current: string;
} }

View File

@@ -1,10 +1,10 @@
import { ISignal } from '@/infrastructure/Events/ISignal'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
export interface IUserFilter { export interface IUserFilter {
readonly currentFilter: IFilterResult | undefined; readonly currentFilter: IFilterResult | undefined;
readonly filtered: ISignal<IFilterResult>; readonly filtered: IEventSource<IFilterResult>;
readonly filterRemoved: ISignal<void>; readonly filterRemoved: IEventSource<void>;
setFilter(filter: string): void; setFilter(filter: string): void;
removeFilter(): void; removeFilter(): void;
} }

View File

@@ -2,12 +2,12 @@ import { IScript } from '@/domain/IScript';
import { FilterResult } from './FilterResult'; import { FilterResult } from './FilterResult';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
import { IUserFilter } from './IUserFilter'; import { IUserFilter } from './IUserFilter';
import { Signal } from '@/infrastructure/Events/Signal'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
export class UserFilter implements IUserFilter { export class UserFilter implements IUserFilter {
public readonly filtered = new Signal<IFilterResult>(); public readonly filtered = new EventSource<IFilterResult>();
public readonly filterRemoved = new Signal<void>(); public readonly filterRemoved = new EventSource<void>();
public currentFilter: IFilterResult | undefined; public currentFilter: IFilterResult | undefined;
constructor(private collection: ICategoryCollection) { constructor(private collection: ICategoryCollection) {

View File

@@ -1,10 +1,10 @@
import { SelectedScript } from './SelectedScript'; import { SelectedScript } from './SelectedScript';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { ISignal } from '@/infrastructure/Events/ISignal'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
export interface IUserSelection { export interface IUserSelection {
readonly changed: ISignal<ReadonlyArray<SelectedScript>>; readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>; readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number; readonly totalSelected: number;
areAllSelected(category: ICategory): boolean; areAllSelected(category: ICategory): boolean;

View File

@@ -2,13 +2,13 @@ import { SelectedScript } from './SelectedScript';
import { IUserSelection } from './IUserSelection'; import { IUserSelection } from './IUserSelection';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { Signal } from '@/infrastructure/Events/Signal'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository'; import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
export class UserSelection implements IUserSelection { export class UserSelection implements IUserSelection {
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>(); public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript>; private readonly scripts: IRepository<string, SelectedScript>;
constructor( constructor(

View File

@@ -1,7 +1,6 @@
import { EventHandler, ISignal } from './ISignal'; import { EventHandler, IEventSource, IEventSubscription } from './IEventSource';
import { IEventSubscription } from './ISubscription';
export class Signal<T> implements ISignal<T> { export class EventSource<T> implements IEventSource<T> {
private handlers = new Map<number, EventHandler<T>>(); private handlers = new Map<number, EventHandler<T>>();
public on(handler: EventHandler<T>): IEventSubscription { public on(handler: EventHandler<T>): IEventSubscription {

View File

@@ -0,0 +1,12 @@
import { IEventSubscription } from './IEventSource';
export class EventSubscriptionCollection {
private readonly subscriptions = new Array<IEventSubscription>();
public register(...subscriptions: IEventSubscription[]) {
this.subscriptions.push(...subscriptions);
}
public unsubscribeAll() {
this.subscriptions.forEach((listener) => listener.unsubscribe());
this.subscriptions.splice(0, this.subscriptions.length);
}
}

View File

@@ -1,6 +1,11 @@
import { IEventSubscription } from './ISubscription'; export interface IEventSource<T> {
export interface ISignal<T> {
on(handler: EventHandler<T>): IEventSubscription; on(handler: EventHandler<T>): IEventSubscription;
} }
export interface IEventSubscription {
unsubscribe(): void;
}
export type EventHandler<T> = (data: T) => void; export type EventHandler<T> = (data: T) => void;

View File

@@ -1,3 +0,0 @@
export interface IEventSubscription {
unsubscribe(): void;
}

View File

@@ -1,7 +1,7 @@
import { Signal } from '../Events/Signal'; import { EventSource } from '../Events/EventSource';
export class AsyncLazy<T> { export class AsyncLazy<T> {
private valueCreated = new Signal(); private valueCreated = new EventSource();
private isValueCreated = false; private isValueCreated = false;
private isCreatingValue = false; private isCreatingValue = false;
private value: T | undefined; private value: T | undefined;
@@ -15,7 +15,7 @@ export class AsyncLazy<T> {
public async getValueAsync(): Promise<T> { public async getValueAsync(): Promise<T> {
// If value is already created, return the value directly // If value is already created, return the value directly
if (this.isValueCreated) { if (this.isValueCreated) {
return Promise.resolve(this.value as T); return Promise.resolve(this.value);
} }
// If value is being created, wait until the value is created and then return it. // If value is being created, wait until the value is created and then return it.
if (this.isCreatingValue) { if (this.isCreatingValue) {

View File

@@ -35,9 +35,7 @@ import MacOsInstructions from './MacOsInstructions.vue';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IApplication } from '@/domain/IApplication';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
@@ -55,8 +53,6 @@ export default class TheCodeButtons extends StatefulVue {
public isMacOsCollection = false; public isMacOsCollection = false;
public fileName = ''; public fileName = '';
private codeListener: IEventSubscription;
public async copyCodeAsync() { public async copyCodeAsync() {
const code = await this.getCurrentCodeAsync(); const code = await this.getCurrentCodeAsync();
Clipboard.copyText(code.current); Clipboard.copyText(code.current);
@@ -68,13 +64,8 @@ export default class TheCodeButtons extends StatefulVue {
this.$modal.show(this.macOsModalName); this.$modal.show(this.macOsModalName);
} }
} }
public destroyed() {
if (this.codeListener) {
this.codeListener.unsubscribe();
}
}
protected initialize(app: IApplication): void { protected initialize(): void {
return; return;
} }
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
@@ -90,12 +81,10 @@ export default class TheCodeButtons extends StatefulVue {
} }
private async react(code: IApplicationCode) { private async react(code: IApplicationCode) {
this.hasCode = code.current && code.current.length > 0; this.hasCode = code.current && code.current.length > 0;
if (this.codeListener) { this.events.unsubscribeAll();
this.codeListener.unsubscribe(); this.events.register(code.changed.on((newCode) => {
}
this.codeListener = code.changed.on((newCode) => {
this.hasCode = newCode && newCode.code.length > 0; this.hasCode = newCode && newCode.code.length > 0;
}); }));
} }
} }

View File

@@ -35,7 +35,6 @@
import { Component, Prop, Watch, Emit } from 'vue-property-decorator'; import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue'; import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component({ @Component({
components: { components: {
@@ -50,17 +49,11 @@ export default class CardListItem extends StatefulVue {
public isAnyChildSelected = false; public isAnyChildSelected = false;
public areAllChildrenSelected = false; public areAllChildrenSelected = false;
private selectionChangedListener: IEventSubscription;
public async mounted() { public async mounted() {
this.updateStateAsync(this.categoryId);
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
this.selectionChangedListener = context.state.selection.changed.on(() => this.updateStateAsync(this.categoryId)); this.events.register(context.state.selection.changed.on(
} () => this.updateSelectionIndicatorsAsync(this.categoryId)));
public destroyed() { await this.updateStateAsync(this.categoryId);
if (this.selectionChangedListener) {
this.selectionChangedListener.unsubscribe();
}
} }
@Emit('selected') @Emit('selected')
public onSelected(isExpanded: boolean) { public onSelected(isExpanded: boolean) {
@@ -81,19 +74,24 @@ export default class CardListItem extends StatefulVue {
@Watch('categoryId') @Watch('categoryId')
public async updateStateAsync(value: |number) { public async updateStateAsync(value: |number) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
const category = !value ? undefined : context.state.collection.findCategory(this.categoryId); const category = !value ? undefined : context.state.collection.findCategory(value);
this.cardTitle = category ? category.name : undefined; this.cardTitle = category ? category.name : undefined;
const currentSelection = context.state.selection; await this.updateSelectionIndicatorsAsync(value);
this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false;
this.areAllChildrenSelected = category ? currentSelection.areAllSelected(category) : false;
} }
protected initialize(): void { protected initialize(): void {
return; return;
} }
protected handleCollectionState(): void { protected handleCollectionState(): void {
// No need, as categoryId will be updated instead
return; return;
} }
private async updateSelectionIndicatorsAsync(categoryId: number) {
const context = await this.getCurrentContextAsync();
const selection = context.state.selection;
const category = context.state.collection.findCategory(categoryId);
this.isAnyChildSelected = category ? selection.isAnySelected(category) : false;
this.areAllChildrenSelected = category ? selection.areAllSelected(category) : false;
}
} }
</script> </script>

View File

@@ -27,8 +27,6 @@ import SelectableTree from './SelectableTree/SelectableTree.vue';
import { INode, NodeType } from './SelectableTree/Node/INode'; import { INode, NodeType } from './SelectableTree/Node/INode';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent'; import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
import { IApplication } from '@/domain/IApplication';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component({ @Component({
components: { components: {
@@ -43,7 +41,6 @@ export default class ScriptsTree extends StatefulVue {
public filterText?: string = null; public filterText?: string = null;
private filtered?: IFilterResult; private filtered?: IFilterResult;
private listeners = new Array<IEventSubscription>();
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) { public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
@@ -75,11 +72,8 @@ export default class ScriptsTree extends StatefulVue {
|| this.filtered.categoryMatches.some( || this.filtered.categoryMatches.some(
(category: ICategory) => node.id === getCategoryNodeId(category)); (category: ICategory) => node.id === getCategoryNodeId(category));
} }
public destroyed() {
this.unsubscribeAll();
}
protected initialize(app: IApplication): void { protected initialize(): void {
return; return;
} }
protected async handleCollectionState(newState: ICategoryCollectionState) { protected async handleCollectionState(newState: ICategoryCollectionState) {
@@ -87,19 +81,18 @@ export default class ScriptsTree extends StatefulVue {
if (!this.categoryId) { if (!this.categoryId) {
this.nodes = parseAllCategories(newState.collection); this.nodes = parseAllCategories(newState.collection);
} }
this.unsubscribeAll(); this.events.unsubscribeAll();
this.subscribe(newState); this.subscribeState(newState);
} }
private subscribe(state: ICategoryCollectionState) { private subscribeState(state: ICategoryCollectionState) {
this.listeners.push(state.selection.changed.on(this.handleSelectionChanged)); this.events.register(
this.listeners.push(state.filter.filterRemoved.on(this.handleFilterRemoved)); state.selection.changed.on(this.handleSelectionChanged),
this.listeners.push(state.filter.filtered.on(this.handleFiltered)); state.filter.filterRemoved.on(this.handleFilterRemoved),
} state.filter.filtered.on(this.handleFiltered),
private unsubscribeAll() { );
this.listeners.forEach((listener) => listener.unsubscribe());
this.listeners.splice(0, this.listeners.length);
} }
private setCurrentFilter(currentFilter: IFilterResult | undefined) { private setCurrentFilter(currentFilter: IFilterResult | undefined) {
if (!currentFilter) { if (!currentFilter) {
this.handleFilterRemoved(); this.handleFilterRemoved();

View File

@@ -13,49 +13,43 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch } from 'vue-property-decorator'; import { Component, Prop, Watch } from 'vue-property-decorator';
import { IReverter } from './Reverter/IReverter'; import { IReverter } from './Reverter/IReverter';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { INode } from './INode'; import { INode } from './INode';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { getReverter } from './Reverter/ReverterFactory'; import { getReverter } from './Reverter/ReverterFactory';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component @Component
export default class RevertToggle extends StatefulVue { export default class RevertToggle extends StatefulVue {
@Prop() public node: INode; @Prop() public node: INode;
public isReverted = false; public isReverted = false;
private handler: IReverter; private handler: IReverter;
private selectionChangeListener: IEventSubscription;
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) { @Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
this.handler = getReverter(node, context.state.collection); this.handler = getReverter(node, context.state.collection);
}
public async onRevertToggledAsync() {
const context = await this.getCurrentContextAsync();
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
}
protected initialize(app: IApplication): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
this.updateStatus(newState.selection.selectedScripts);
if (this.selectionChangeListener) {
this.selectionChangeListener.unsubscribe();
}
this.selectionChangeListener = newState.selection.changed.on(
(scripts) => this.updateStatus(scripts));
}
private updateStatus(scripts: ReadonlyArray<SelectedScript>) {
this.isReverted = this.handler.getState(scripts);
}
} }
public async onRevertToggledAsync() {
const context = await this.getCurrentContextAsync();
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
}
protected initialize(): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState): void {
this.updateStatus(newState.selection.selectedScripts);
this.events.unsubscribeAll();
this.events.register(newState.selection.changed.on((scripts) => this.updateStatus(scripts)));
}
private updateStatus(scripts: ReadonlyArray<SelectedScript>) {
this.isReverted = this.handler.getState(scripts);
}
}
</script> </script>

View File

@@ -50,7 +50,6 @@ import { Grouping } from './Grouping/Grouping';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
/** Shows content of single category or many categories */ /** Shows content of single category or many categories */
@Component({ @Component({
@@ -79,11 +78,6 @@ export default class TheScripts extends StatefulVue {
public isSearching = false; public isSearching = false;
public searchHasMatches = false; public searchHasMatches = false;
private listeners = new Array<IEventSubscription>();
public destroyed() {
this.unsubscribeAll();
}
public async clearSearchQueryAsync() { public async clearSearchQueryAsync() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
const filter = context.state.filter; const filter = context.state.filter;
@@ -97,23 +91,21 @@ export default class TheScripts extends StatefulVue {
this.repositoryUrl = app.info.repositoryWebUrl; this.repositoryUrl = app.info.repositoryWebUrl;
} }
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
this.unsubscribeAll(); this.events.unsubscribeAll();
this.subscribe(newState); this.subscribeState(newState);
} }
private subscribe(state: ICategoryCollectionState) { private subscribeState(state: ICategoryCollectionState) {
this.listeners.push(state.filter.filterRemoved.on(() => { this.events.register(
this.isSearching = false; state.filter.filterRemoved.on(() => {
})); this.isSearching = false;
state.filter.filtered.on((result: IFilterResult) => { }),
this.searchQuery = result.query; state.filter.filtered.on((result: IFilterResult) => {
this.isSearching = true; this.searchQuery = result.query;
this.searchHasMatches = result.hasAnyMatches(); this.isSearching = true;
}); this.searchHasMatches = result.hasAnyMatches();
} }),
private unsubscribeAll() { );
this.listeners.forEach((listener) => listener.unsubscribe());
this.listeners.splice(0, this.listeners.length);
} }
} }
</script> </script>

View File

@@ -2,10 +2,10 @@ import { Component, Vue } from 'vue-property-decorator';
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { buildContext } from '@/application/Context/ApplicationContextProvider'; import { buildContext } from '@/application/Context/ApplicationContextProvider';
import { IApplicationContextChangedEvent } from '../application/Context/IApplicationContext'; import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { ICategoryCollectionState } from '../application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscription } from '../infrastructure/Events/ISubscription'; import { EventSubscriptionCollection } from '../infrastructure/Events/EventSubscriptionCollection';
// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91 // @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
@Component @Component
@@ -13,18 +13,19 @@ export abstract class StatefulVue extends Vue {
public static instance = new AsyncLazy<IApplicationContext>( public static instance = new AsyncLazy<IApplicationContext>(
() => Promise.resolve(buildContext())); () => Promise.resolve(buildContext()));
private listener: IEventSubscription; protected readonly events = new EventSubscriptionCollection();
private readonly ownEvents = new EventSubscriptionCollection();
public async mounted() { public async mounted() {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
this.listener = context.contextChanged.on((event) => this.handleStateChangedEvent(event)); this.ownEvents.register(context.contextChanged.on((event) => this.handleStateChangedEvent(event)));
this.initialize(context.app); this.initialize(context.app);
this.handleCollectionState(context.state, undefined); this.handleCollectionState(context.state, undefined);
} }
public destroyed() { public destroyed() {
if (this.listener) { this.ownEvents.unsubscribeAll();
this.listener.unsubscribe(); this.events.unsubscribeAll();
}
} }
protected abstract initialize(app: IApplication): void; protected abstract initialize(app: IApplication): void;

View File

@@ -11,9 +11,6 @@ import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeC
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IApplication } from '@/domain/IApplication';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory'; import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
@Component @Component
@@ -22,16 +19,14 @@ export default class TheCodeArea extends StatefulVue {
private editor!: ace.Ace.Editor; private editor!: ace.Ace.Editor;
private currentMarkerId?: number; private currentMarkerId?: number;
private codeListener: IEventSubscription;
@Prop() private theme!: string; @Prop() private theme!: string;
public destroyed() { public destroyed() {
this.unsubscribeCodeListening();
this.destroyEditor(); this.destroyEditor();
} }
protected initialize(app: IApplication): void { protected initialize(): void {
return; return;
} }
protected handleCollectionState(newState: ICategoryCollectionState): void { protected handleCollectionState(newState: ICategoryCollectionState): void {
@@ -39,18 +34,10 @@ export default class TheCodeArea extends StatefulVue {
this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language); this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language);
const appCode = newState.code; const appCode = newState.code;
this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1); this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1);
this.unsubscribeCodeListening(); this.events.unsubscribeAll();
this.subscribe(appCode); this.events.register(appCode.changed.on((code) => this.updateCodeAsync(code)));
} }
private subscribe(appCode: IApplicationCode) {
this.codeListener = appCode.changed.on((code) => this.updateCodeAsync(code));
}
private unsubscribeCodeListening() {
if (this.codeListener) {
this.codeListener.unsubscribe();
}
}
private async updateCodeAsync(event: ICodeChangedEvent) { private async updateCodeAsync(event: ICodeChangedEvent) {
this.removeCurrentHighlighting(); this.removeCurrentHighlighting();
if (event.isEmpty()) { if (event.isEmpty()) {

View File

@@ -15,9 +15,7 @@ import { StatefulVue } from './StatefulVue';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective'; import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter'; import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { IApplication } from '@/domain/IApplication';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
@Component( { @Component( {
directives: { NonCollapsing }, directives: { NonCollapsing },
@@ -27,8 +25,6 @@ export default class TheSearchBar extends StatefulVue {
public searchPlaceHolder = 'Search'; public searchPlaceHolder = 'Search';
public searchQuery = ''; public searchQuery = '';
private readonly listeners = new Array<IEventSubscription>();
@Watch('searchQuery') @Watch('searchQuery')
public async updateFilterAsync(newFilter: |string) { public async updateFilterAsync(newFilter: |string) {
const context = await this.getCurrentContextAsync(); const context = await this.getCurrentContextAsync();
@@ -39,28 +35,21 @@ export default class TheSearchBar extends StatefulVue {
filter.setFilter(newFilter); filter.setFilter(newFilter);
} }
} }
public destroyed() {
this.unsubscribeAll();
}
protected initialize(app: IApplication): void { protected initialize(): void {
return; return;
} }
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) { protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) {
const totalScripts = newState.collection.totalScripts; const totalScripts = newState.collection.totalScripts;
this.searchPlaceHolder = `Search in ${totalScripts} scripts`; this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : ''; this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : '';
this.unsubscribeAll(); this.events.unsubscribeAll();
this.subscribe(newState.filter); this.subscribeFilter(newState.filter);
} }
private subscribe(filter: IUserFilter) { private subscribeFilter(filter: IUserFilter) {
this.listeners.push(filter.filtered.on((result) => this.handleFiltered(result))); this.events.register(filter.filtered.on((result) => this.handleFiltered(result)));
this.listeners.push(filter.filterRemoved.on(() => this.handleFilterRemoved())); this.events.register(filter.filterRemoved.on(() => this.handleFilterRemoved()));
}
private unsubscribeAll() {
this.listeners.forEach((listener) => listener.unsubscribe());
this.listeners.splice(0, this.listeners.length);
} }
private handleFiltered(result: IFilterResult) { private handleFiltered(result: IFilterResult) {
this.searchQuery = result.query; this.searchQuery = result.query;

View File

@@ -1,45 +1,43 @@
import { ISignal } from '@/infrastructure/Events/ISignal'; import { EventHandler, IEventSource, IEventSubscription } from '@/infrastructure/Events/IEventSource';
import { IEventSubscription } from '@/infrastructure/Events/ISubscription'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { Signal } from '@/infrastructure/Events/Signal';
import { expect } from 'chai'; import { expect } from 'chai';
import { EventHandler } from '@/infrastructure/Events/ISignal'; import 'mocha';
describe('EventSource', () => {
describe('Signal', () => {
class ObserverMock { class ObserverMock {
public readonly onReceiveCalls = new Array<number>(); public readonly onReceiveCalls = new Array<number>();
public readonly callbacks = new Array<EventHandler<number>>(); public readonly callbacks = new Array<EventHandler<number>>();
public readonly subscription: IEventSubscription; public readonly subscription: IEventSubscription;
constructor(subject: ISignal<number>) { constructor(subject: IEventSource<number>) {
this.callbacks.push((arg) => this.onReceiveCalls.push(arg)); this.callbacks.push((arg) => this.onReceiveCalls.push(arg));
this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg))); this.subscription = subject.on((arg) => this.callbacks.forEach((action) => action(arg)));
} }
} }
let signal: Signal<number>; let sut: EventSource<number>;
beforeEach(() => signal = new Signal()); beforeEach(() => sut = new EventSource());
describe('single observer', () => { describe('single observer', () => {
// arrange // arrange
let observer: ObserverMock; let observer: ObserverMock;
beforeEach(() => { beforeEach(() => {
observer = new ObserverMock(signal); observer = new ObserverMock(sut);
}); });
it('notify() executes the callback', () => { it('notify() executes the callback', () => {
// act // act
signal.notify(5); sut.notify(5);
// assert // assert
expect(observer.onReceiveCalls).to.have.length(1); expect(observer.onReceiveCalls).to.have.length(1);
}); });
it('notify() executes the callback with the payload', () => { it('notify() executes the callback with the payload', () => {
const expected = 5; const expected = 5;
// act // act
signal.notify(expected); sut.notify(expected);
// assert // assert
expect(observer.onReceiveCalls).to.deep.equal([expected]); expect(observer.onReceiveCalls).to.deep.equal([expected]);
}); });
it('notify() does not call callback when unsubscribed', () => { it('notify() does not call callback when unsubscribed', () => {
// act // act
observer.subscription.unsubscribe(); observer.subscription.unsubscribe();
signal.notify(5); sut.notify(5);
// assert // assert
expect(observer.onReceiveCalls).to.have.lengthOf(0); expect(observer.onReceiveCalls).to.have.lengthOf(0);
}); });
@@ -50,13 +48,13 @@ describe('Signal', () => {
let observers: ObserverMock[]; let observers: ObserverMock[];
beforeEach(() => { beforeEach(() => {
observers = [ observers = [
new ObserverMock(signal), new ObserverMock(signal), new ObserverMock(sut), new ObserverMock(sut),
new ObserverMock(signal), new ObserverMock(signal), new ObserverMock(sut), new ObserverMock(sut),
]; ];
}); });
it('notify() should execute all callbacks', () => { it('notify() should execute all callbacks', () => {
// act // act
signal.notify(5); sut.notify(5);
// assert // assert
observers.forEach((observer) => { observers.forEach((observer) => {
expect(observer.onReceiveCalls).to.have.length(1); expect(observer.onReceiveCalls).to.have.length(1);
@@ -65,7 +63,7 @@ describe('Signal', () => {
it('notify() should execute all callbacks with payload', () => { it('notify() should execute all callbacks with payload', () => {
const expected = 5; const expected = 5;
// act // act
signal.notify(expected); sut.notify(expected);
// assert // assert
observers.forEach((observer) => { observers.forEach((observer) => {
expect(observer.onReceiveCalls).to.deep.equal([expected]); expect(observer.onReceiveCalls).to.deep.equal([expected]);
@@ -79,7 +77,7 @@ describe('Signal', () => {
observers[i].callbacks.push(() => actualSequence.push(i)); observers[i].callbacks.push(() => actualSequence.push(i));
} }
// act // act
signal.notify(5); sut.notify(5);
// assert // assert
expect(actualSequence).to.deep.equal(expectedSequence); expect(actualSequence).to.deep.equal(expectedSequence);
}); });

View File

@@ -0,0 +1,22 @@
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
import { expect } from 'chai';
import 'mocha';
describe('EventSubscriptionCollection', () => {
it('unsubscribeAll unsubscribes from all registered subscriptions', () => {
// arrange
const sut = new EventSubscriptionCollection();
const expected = [ 'unsubscribed1', 'unsubscribed2'];
const actual = new Array<string>();
const subscriptions: IEventSubscription[] = [
{ unsubscribe: () => actual.push(expected[0]) },
{ unsubscribe: () => actual.push(expected[1]) },
];
// act
sut.register(...subscriptions);
sut.unsubscribeAll();
// assert
expect(actual).to.deep.equal(expected);
});
});