diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0c5a793..beded30 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,6 @@ import { Component } from '@angular/core'; +import { InnerNode, InnerNodeState } from './store/inner-node'; +import { Root } from './store/root'; @Component({ selector: 'app-root', @@ -6,5 +8,26 @@ import { Component } from '@angular/core'; styleUrls: ['./app.component.scss'] }) export class AppComponent { - title = 'frontend'; + title = 'life'; + + /* tests + constructor() { + + const root = new Root(); + root.log(); + + const l = new InnerNode(); + const r = new InnerNode(); + root.addChildren([l, r]); + root.log(); + + const rl = new InnerNode(); + const rr = new InnerNode(); + r.addChildren([rl, rr]); + root.log(); + + rr.changeKeys({ dummy: 8 }); + root.log(); + } + */ } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4fdc9d0..a9173db 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -23,6 +23,8 @@ import { RemoveBlockComponent } from './components/modal/modals/remove-block/rem import { ToggleComponent } from './components/shared/toggle/toggle.component'; import { TasksComponent } from './components/pages/page/tower/tasks/tasks.component'; import { ColorPipe } from './pipes/color.pipe'; +import { Root } from './store/root'; +import { InnerNode, InnerNodeState } from './store/inner-node'; @NgModule({ declarations: [ diff --git a/src/app/components/pages/page/tower/block/block.component.ts b/src/app/components/pages/page/tower/block/block.component.ts index b539ec1..80961fd 100644 --- a/src/app/components/pages/page/tower/block/block.component.ts +++ b/src/app/components/pages/page/tower/block/block.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { Block } from '../../../../../model/block'; +import { Block, BlockState } from '../../../../../model/block'; import { ModalService } from '../../../../../services/modal.service'; import { ColoredBlock, Tower } from '../../../../../model/tower'; @@ -23,7 +23,7 @@ export class BlockComponent { isDone: this.block.isDone }); console.log(description); - this.block.changeProperties({ + this.block.changeKeys({ tag: selected, description, isDone diff --git a/src/app/components/pages/page/tower/tasks/tasks.component.ts b/src/app/components/pages/page/tower/tasks/tasks.component.ts index 17eb6e8..1814f1b 100644 --- a/src/app/components/pages/page/tower/tasks/tasks.component.ts +++ b/src/app/components/pages/page/tower/tasks/tasks.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; -import { Block } from '../../../../../model/block'; +import { Block, BlockState } from '../../../../../model/block'; import { Tower } from '../../../../../model/tower'; import { ModalService } from '../../../../../services/modal.service'; import { CancelService } from '../../../../../services/cancel.service'; @@ -55,7 +55,7 @@ export class TasksComponent implements OnInit { change.created = new Date(); } - block.changeProperties(change); + block.changeKeys(change); } catch { // pass } diff --git a/src/app/model/block.ts b/src/app/model/block.ts index 2d49058..eab92c4 100644 --- a/src/app/model/block.ts +++ b/src/app/model/block.ts @@ -1,30 +1,21 @@ import { Serializable } from './serializable'; import { IBlock } from '../interfaces/persistance/block'; import { Node } from '../store/node'; +import { InnerNodeState } from '../store/inner-node'; -export class Block extends Serializable implements IBlock { - constructor(parent: Node, props: IBlock) { - super(parent, props, 'Block'); - this.onAfterClone(); - } +export interface BlockState extends IBlock, InnerNodeState {} - protected onAfterClone(): void { - if (this.created.constructor.name !== 'Date') { - this.created = new Date(this.created); - } - - // TODO: remove. - if (this.isDone === null || this.isDone === undefined) { - this.isDone = false; - } - } - - changeProperties(values: Partial) { - this.changeKeys(values); - } - - created: Date; - isDone: boolean; +export class Block extends Serializable implements IBlock, BlockState { + readonly created: Date; + readonly isDone: boolean; readonly description: string; readonly tag: string; + + constructor(props: IBlock) { + console.log('b'); + if (props.created.constructor.name !== 'Date') { + props.created = new Date(props.created); + } + super(props, 'Block'); + } } diff --git a/src/app/model/page.ts b/src/app/model/page.ts index ba12c64..b9493c9 100644 --- a/src/app/model/page.ts +++ b/src/app/model/page.ts @@ -1,17 +1,12 @@ import { Serializable } from './serializable'; import { IPage } from '../interfaces/persistance/page'; import { Tower } from './tower'; -import { Node } from '../store/node'; +import { InnerNodeState } from '../store/inner-node'; -export class Page extends Serializable implements IPage { - constructor(parent: Node, props: IPage) { - super(parent, props, 'Page'); - } +export interface PageState extends InnerNodeState, IPage {} +export class Page extends Serializable implements IPage, PageState { readonly name: string; - get towers(): Array { - return this.children as Array; - } readonly userData: { hideCreateTowerButton: boolean; @@ -21,10 +16,17 @@ export class Page extends Serializable implements IPage { }; }; + constructor(props: IPage) { + super(props, 'Page'); + } + + get towers(): Array { + return this.children as Array; + } + setHideCreateTowerButton(value: boolean) { - this.changeKey({ - propertyName: 'userData', - value: { + this.changeKeys({ + userData: { ...this.userData, hideCreateTowerButton: value } @@ -41,9 +43,8 @@ export class Page extends Serializable implements IPage { towers.splice(previousIndex, 1); towers.splice(currentIndex, 0, tower); - this.changeValue({ - oldValue: this.towers, - newValue: towers + this.changeKeys({ + children: towers }); } @@ -53,17 +54,18 @@ export class Page extends Serializable implements IPage { hue = Math.random() * 360; } while (30 <= hue && hue <= 200); - new Tower(this, { - name, - blocks: [], - baseColor: { h: hue, s: 100, l: 50 } - }); + this.addChildren([ + new Tower({ + name, + blocks: [], + baseColor: { h: hue, s: 100, l: 50 } + }) + ]); } removeTower(tower: Tower) { - this.changeValue({ - oldValue: this.towers, - newValue: this.towers.filter(t => t !== tower) + this.changeKeys({ + towers: this.towers.filter(t => t !== tower) }); } } diff --git a/src/app/model/serializable.ts b/src/app/model/serializable.ts index 8eaae74..52c9834 100644 --- a/src/app/model/serializable.ts +++ b/src/app/model/serializable.ts @@ -1,10 +1,6 @@ -import { Cloneable } from '../store/cloneable'; -import { Node } from '../store/node'; +import { InnerNode } from '../store/inner-node'; -export class Serializable extends Cloneable { - protected type: string; - - private static propertyList: any = {}; +export class Serializable extends InnerNode { static childrenMap: { [type: string]: { childrenConstructor: typeof Serializable; @@ -13,12 +9,11 @@ export class Serializable extends Cloneable { }; }; - protected onAfterClone(): void { - // pass - } + private static propertyList: any = {}; + protected type: string; - protected constructor(parent: Node, properties: any, type: string) { - super(parent); + protected constructor(properties: any, type: string) { + super(); const compiledType = this.constructor.name; if (!Serializable.propertyList.hasOwnProperty(compiledType)) { @@ -28,16 +23,15 @@ export class Serializable extends Cloneable { if (properties.hasOwnProperty(property)) { const propertyValue = properties[property]; // This should be ran after the original constructor has finished. - console.log(type); if (property === Serializable.childrenMap[type].childrenListName) { new Promise(r => r()).then(() => { - for (let child of propertyValue) { - new Serializable.childrenMap[type].childrenConstructor( - this, - child, - Serializable.childrenMap[type].childrenType - ); - } + const children = propertyValue.map( + c => + new Serializable.childrenMap[type].childrenConstructor(c, Serializable.childrenMap[type].childrenType) + ); + console.log(type, 'created'); + this.addChildren(children); + console.log(type, 'added'); }); } else { this[property] = properties[property]; diff --git a/src/app/model/tower.ts b/src/app/model/tower.ts index 7161212..fea3fac 100644 --- a/src/app/model/tower.ts +++ b/src/app/model/tower.ts @@ -3,16 +3,16 @@ import { lighten } from '../utils/color'; import { Block } from './block'; import { Serializable } from './serializable'; import { hash } from '../utils/hash'; -import { Node } from '../store/node'; import { IColor } from '../interfaces/color'; +import { InnerNodeState } from '../store/inner-node'; export type ColoredBlock = Block & { color: IColor }; -export class Tower extends Serializable implements ITower { - protected type = 'Tower'; +export interface TowerState extends ITower, InnerNodeState {} +export class Tower extends Serializable implements ITower, TowerState { tags: string[]; - name: string; + readonly name: string; get blocks(): Array { return this.children as Array; @@ -22,12 +22,11 @@ export class Tower extends Serializable implements ITower { readonly baseColor: IColor; - constructor(parent: Node, props: ITower) { - super(parent, props, 'Tower'); - this.onAfterClone(); + constructor(props: ITower) { + super(props, 'Tower'); } - protected onAfterClone(): void { + protected onAfterClone() { this.blocks.sort((a, b) => { return a.created.getTime() - b.created.getTime(); }); @@ -47,15 +46,15 @@ export class Tower extends Serializable implements ITower { } addBlock(props: { tag: string; description: string; isDone: boolean }) { - new Block(this, { - created: new Date(), - ...props - }); + this.addChildren([ + new Block({ + created: new Date(), + ...props + }) + ]); } - changeName(newName: string) { - // For optimization purposes. - this.name = newName; - this.mutatedUpdate(); + changeName(name: string) { + this.changeKeys({ name }); } } diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts index 8c01ba4..555acc3 100644 --- a/src/app/services/data.service.ts +++ b/src/app/services/data.service.ts @@ -13,10 +13,6 @@ import { Observable } from 'rxjs/internal/Observable'; providedIn: 'root' }) export class DataService extends Root { - get pages(): Array { - return this.children; - } - private readonly _safeChildren: BehaviorSubject> = new BehaviorSubject(null); readonly safeChildren$: Observable> = this._safeChildren.asObservable(); @@ -25,6 +21,30 @@ export class DataService extends Root { this.init().catch(); } + get pages(): Array { + return this.children; + } + + save(timeout: number) { + this.storeService.scheduleSave(this.pages, timeout); + } + + addPage(name: string) { + const page = new Page({ + name, + userData: {}, + towers: [] + }); + this.addChildren([page]); + page.addTower(); + } + + removePage(page: Page) { + this.changeKeys({ + children: this.children.filter(c => c !== page) + }); + } + private async init() { const pages = await this.storeService.load(); Serializable.childrenMap = { @@ -44,43 +64,15 @@ export class DataService extends Root { childrenType: null } }; + this.children$.subscribe(value => { + this.log(); + }); - for (let page of pages) { - new Page(this, page); - } - setTimeout(() => { - this.children$.subscribe(value => { - this.log(); - }); - }, 0); + this.addChildren(pages.map(p => new Page(p))); this.children$.subscribe(value => { this._safeChildren.next(value); this.save(0); }); } - - mutatedUpdate() { - this.save(2500); - } - - save(timeout: number) { - this.storeService.scheduleSave(this.pages, timeout); - } - - addPage(name: string) { - const page = new Page(this, { - name, - userData: {}, - towers: [] - }); - page.addTower(); - } - - removePage(page: Page) { - this.changeValue({ - oldValue: this.children, - newValue: this.children.filter(c => c !== page) - }); - } } diff --git a/src/app/services/store.service.ts b/src/app/services/store.service.ts index a4b1596..819e04b 100644 --- a/src/app/services/store.service.ts +++ b/src/app/services/store.service.ts @@ -95,7 +95,7 @@ export class StoreService { constructor() { const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY); - this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T; + this.storedData = JSON.parse(false ? localStorageData : this.mockData) as T; } scheduleSave(data: T, timeout: number) { diff --git a/src/app/store/cloneable.ts b/src/app/store/cloneable.ts deleted file mode 100644 index 317e279..0000000 --- a/src/app/store/cloneable.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { InnerNode } from './inner-node'; -import { Node } from './node'; - -export abstract class Cloneable extends InnerNode { - protected constructor(parent: Node) { - super(parent); - } - - protected abstract onAfterClone(): void; - - protected cloneWithMap(map: (node: this) => void): this { - const insides = Object.getOwnPropertyDescriptors(this); - - const insidesProxy = new Proxy(insides, { - get: (target, prop, proxy) => { - if (prop == '__target__') { - return target; - } - if (target.hasOwnProperty(prop)) { - const value = target[prop as string].value; - if (typeof value === 'function') { - return value.bind(proxy); - } - return value; - } else if (target.prototype.hasOwnProperty(prop)) { - const value = target.prototype[prop]; - if (typeof value === 'function') { - return value.bind(proxy); - } - return value; - } - }, - set: (target, prop, value) => { - return (target[prop as string].value = value); - } - }); - map(insidesProxy); - - return this.cloneFromInsides(insidesProxy.__target__); - } - - protected cloneWithAdd({ propertyName, value }: { value: any; propertyName: string }): this { - if (this[propertyName] === value) { - return this; - } - - const insides = Object.getOwnPropertyDescriptors(this); - insides[propertyName].value = value; - return this.cloneFromInsides(insides); - } - - protected cloneWithChangedKeys(props: { [propertyName: string]: any }): this { - const insides = Object.getOwnPropertyDescriptors(this); - - for (let key in props) { - if (props.hasOwnProperty(key)) { - if (insides.hasOwnProperty(key)) { - insides[key].value = props[key]; - } else { - // @ts-ignore - insides[key] = { - value: props[key] - }; - } - } - } - - return this.cloneFromInsides(insides); - } - - protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this { - if (oldValue === newValue) { - return this; - } - - const insides = Object.getOwnPropertyDescriptors(this); - - let wasMatch = false; - for (let name in insides) { - if (insides.hasOwnProperty(name) && insides[name].value === oldValue) { - insides[name].value = newValue; - wasMatch = true; - } - } - - if (!wasMatch) { - throw new TypeError(`Object has no property with value: ${oldValue.toString()}`); - } - - return this.cloneFromInsides(insides); - } - - private cloneFromInsides(insides): this { - insides.id.value = Node.id++; - insides.copyCount.value++; - Node.sumCopyCount++; - - const clone = Object.create(Object.getPrototypeOf(this), insides); - clone.onAfterClone(); - return clone; - } -} diff --git a/src/app/store/inner-node.ts b/src/app/store/inner-node.ts index 6f37af8..66409ba 100644 --- a/src/app/store/inner-node.ts +++ b/src/app/store/inner-node.ts @@ -1,9 +1,18 @@ -import { Node } from './node'; +import { Node, NodeState } from './node'; -export abstract class InnerNode extends Node { - readonly children: Array = []; - protected parent: Node; +export interface InnerNodeState extends NodeState { + dummy: any; +} + +export class InnerNode extends Node implements InnerNodeState { + readonly dummy = 3; + parent: Node; private nextVersion: this = null; + readonly children: Array = []; + + constructor() { + super(); + } get latestVersion(): this { let version; @@ -13,40 +22,34 @@ export abstract class InnerNode extends Node { return version; } - mutatedUpdate() { - this.parent.mutatedUpdate(); + addChildren(children: Array) { + super.addChildren.call(this.latestVersion, children); } - map(map: (a: this) => void) { - return this.update((self: this) => this.cloneWithMap.call(self, map)); - } - - changeKeys(props: { [propertyName: string]: any }): this { - return this.update((self: this) => this.cloneWithChangedKeys.call(self, props)); - } - - addChild(update: { child: InnerNode }) { - super.addChild.call(this.latestVersion, update); - } - - changeChild(update: { oldValue: InnerNode; newValue: InnerNode }) { + replaceChild(update: { oldValue: InnerNode; newValue: InnerNode }) { super.replaceChild.call(this.latestVersion, update); } - protected abstract cloneWithMap(map: (a: this) => void): this; - protected abstract cloneWithChangedKeys(props: { [propertyName: string]: any }): this; - - private update(cloneMethod: (self: this) => this): this { + changeKeys(props: Partial): this { if (this.nextVersion !== null) { - this.latestVersion.update(cloneMethod); + this.latestVersion.changeKeys(props); } - const clone = cloneMethod(this); - if (clone === this) { - return this; + const clone = this.cloneWithChangedKeys(props); + + let shouldClone = false; + for (const prop in props) { + // @ts-ignore + if (props.hasOwnProperty(prop) && props[prop] !== this[prop]) { + shouldClone = true; + break; + } + } + if (!shouldClone) { + return; } - for (let child of clone.children) { + for (const child of clone.children) { child.parent = clone; } @@ -58,4 +61,28 @@ export abstract class InnerNode extends Node { this.nextVersion = clone; return clone; } + + protected onAfterClone() {} + + protected cloneWithChangedKeys(props: Partial): this { + const insides = Object.getOwnPropertyDescriptors(this); + + for (const key in props) { + if (props.hasOwnProperty(key)) { + if (insides.hasOwnProperty(key)) { + insides[key].value = props[key]; + } else { + // @ts-ignore + insides[key] = { + value: props[key] + }; + } + } + } + + const clone = Object.create(Object.getPrototypeOf(this), insides); + clone.initiate(); + clone.onAfterClone(); + return clone; + } } diff --git a/src/app/store/node.ts b/src/app/store/node.ts index 1bee7cd..fc6363f 100644 --- a/src/app/store/node.ts +++ b/src/app/store/node.ts @@ -1,22 +1,24 @@ -import { InnerNode } from './inner-node'; import { Unique } from './unique'; +import { InnerNode } from './inner-node'; -export abstract class Node extends Unique { - readonly children: Array; - // TODO: fix types. - protected abstract changeKeys(props: any): this; - abstract mutatedUpdate(): void; +export interface NodeState { + children: Array; +} - private copyCount = 0; +export abstract class Node extends Unique implements NodeState { + protected copyCount = 1; + abstract readonly children: Array; + + protected abstract changeKeys(props: Partial): this; protected initiate() { super.initiate(); - this.copyCount++; + ++this.copyCount; } - addChild({ child }: { child: InnerNode }) { - this.changeKeys({ - children: [...this.children, child] + addChildren(children: Array) { + this.changeKeys({ + children: [...this.children, ...children] }); } @@ -25,7 +27,7 @@ export abstract class Node extends Unique { return; } - this.changeKeys({ + this.changeKeys({ children: this.children.map(c => (c === oldValue ? newValue : c)) }); } @@ -33,7 +35,7 @@ export abstract class Node extends Unique { protected _log(indent = ''): string { const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`; let response = `${basicInfo}${' '.repeat(25 - basicInfo.length)}siblings: ${this.copyCount}\n`; - for (let c of this.children) { + for (const c of this.children) { response += `${c._log(indent + ' ')}`; } return response; diff --git a/src/app/store/root.ts b/src/app/store/root.ts index 54c62e8..7483da1 100644 --- a/src/app/store/root.ts +++ b/src/app/store/root.ts @@ -1,12 +1,16 @@ import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { Observable } from 'rxjs/internal/Observable'; -import { Node } from './node'; +import { Node, NodeState } from './node'; import { InnerNode } from './inner-node'; export class Root extends Node { private readonly _children: BehaviorSubject> = new BehaviorSubject([]); readonly children$: Observable> = this._children.asObservable(); + constructor() { + super(); + } + get children(): Array { return this._children.getValue(); } @@ -15,27 +19,14 @@ export class Root extends Node { this._children.next(value); } - mutatedUpdate() { - // pass - } - - changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) { - if (this.children !== oldValue) { - throw new TypeError('Only children can be changed.'); - } - this.children = newValue; - for (let child of this.children) { - child.parent = this; - } - } - - changeKey({ propertyName, value }: { propertyName: string; value: any }) { - if (propertyName !== 'children') { - throw new TypeError('Only children can be changed.'); - } - this.children = value; - for (let child of this.children) { - child.parent = this; + changeKeys(props: Partial): this { + if (props.hasOwnProperty('children')) { + // @ts-ignore + this.children = props.children; + for (const child of this.children) { + child.parent = this; + } } + return this; } } diff --git a/src/app/store/unique.ts b/src/app/store/unique.ts index a8228e4..a0500c1 100644 --- a/src/app/store/unique.ts +++ b/src/app/store/unique.ts @@ -6,10 +6,10 @@ export abstract class Unique extends Initiable { return Unique.nextId; } + private _id: number; get id(): number { return this._id; } - private _id: number; protected initiate() { this._id = Unique.nextId++;