From 4d92d5ba08bdece9e79e7f385f57afbbccda6702 Mon Sep 17 00:00:00 2001 From: "pengfei.zhou" Date: Fri, 25 Oct 2019 02:19:59 +0800 Subject: [PATCH 1/3] mvvm and candies --- js-framework/index.ts | 3 +- js-framework/src/util/candies.ts | 100 ++++++++++++++++++++++++++++++ js-framework/src/vm/mvvm.ts | 103 ++++++++++++------------------- 3 files changed, 142 insertions(+), 64 deletions(-) create mode 100644 js-framework/src/util/candies.ts diff --git a/js-framework/index.ts b/js-framework/index.ts index 66dcb3ff..39b32acf 100644 --- a/js-framework/index.ts +++ b/js-framework/index.ts @@ -19,5 +19,6 @@ export * from "./src/util/color" export * from './src/util/log' export * from './src/util/types' export * from './src/util/gravity' +export * from './src/util/candies' export * from './src/vm/mvvm' -export * from './src/runtime/global' \ No newline at end of file +export * from './src/runtime/global' diff --git a/js-framework/src/util/candies.ts b/js-framework/src/util/candies.ts new file mode 100644 index 00000000..4543b728 --- /dev/null +++ b/js-framework/src/util/candies.ts @@ -0,0 +1,100 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { View, Stack, VLayout, HLayout } from "../ui/view" + +export type ViewBlock = () => View + +export function stack(blocks: ViewBlock[]) { + return takeAlso(new Stack)( + it => { + for (let block of blocks) { + it.addChild(block()) + } + }) +} + +export function vlayout(blocks: ViewBlock[]) { + return takeAlso(new VLayout)( + it => { + for (let block of blocks) { + it.addChild(block()) + } + }) +} + +export function hlayout(blocks: ViewBlock[]) { + return takeAlso(new HLayout)( + it => { + for (let block of blocks) { + it.addChild(block()) + } + }) +} + +export function take(target: T) { + return (block: (p: T) => void) => { + block(target) + } +} + +export function takeNonNull(target?: T) { + return (block: (p: T) => R) => { + if (target !== undefined) { + return block(target) + } + } +} + +export function takeNull(target?: T) { + return (block: () => R) => { + if (target === undefined) { + return block() + } + } +} + +export function takeLet(target: T) { + return (block: (p: T) => R | undefined) => { + return block(target) + } +} + +export function takeAlso(target: T) { + return (block: (p: T) => void) => { + block(target) + return target + } +} + +export function takeIf(target: T) { + return (predicate: (t: T) => boolean) => { + return predicate(target) ? target : undefined + } +} + +export function takeUnless(target: T) { + return (predicate: (t: T) => boolean) => { + return predicate(target) ? undefined : target + } +} + +export function repeat(action: (count: number) => void) { + return (times: number) => { + for (let i = 0; i < times; i++) { + action(i) + } + } +} \ No newline at end of file diff --git a/js-framework/src/vm/mvvm.ts b/js-framework/src/vm/mvvm.ts index 70feae8f..e31b4576 100644 --- a/js-framework/src/vm/mvvm.ts +++ b/js-framework/src/vm/mvvm.ts @@ -13,84 +13,61 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { View, Group } from "../ui/view"; +import { Group } from "../ui/view"; import { Panel } from "../ui/panel"; -function listen(obj: T, listener: Function): T { - return new Proxy(obj, { - get: (target, prop, receiver) => { - const ret = Reflect.get(target, prop, receiver) - if (ret instanceof Function) { - return Reflect.get(target, prop, receiver) - } else if (ret instanceof Object) { - return listen(ret, listener) - } else { - return ret - } - }, - - set: (target, prop, value, receiver) => { - const ret = Reflect.set(target, prop, value, receiver) - Reflect.apply(listener, undefined, []) - return ret - }, - }) -} - -export abstract class ViewHolder { +export abstract class ViewHolder{ abstract build(root: Group): void + abstract bind(state: M): void } -export abstract class VMPanel extends Panel { +export type Setter = (state: M) => void - private vm?: ViewModel +export abstract class ViewModel> { + private state: M + private viewHolder: V - abstract getVMClass(): new (m: M, v: V) => ViewModel + constructor(obj: M, v: V) { + this.state = obj + this.viewHolder = v + } + getState() { + return this.state + } - abstract getModel(): M + updateState(setter: Setter) { + setter(this.state) + this.viewHolder.bind(this.state) + } - abstract getViewHolder(): V + attach(view: Group) { + this.viewHolder.build(view) + } +} +export type ViewModelClass = new (m: M, v: ViewHolder) => ViewModel> - getVM() { +export type ViewHolderClass = new () => ViewHolder + +export abstract class VMPanel extends Panel { + + private vm?: ViewModel> + private vh?: ViewHolder + + abstract getViewModelClass(): ViewModelClass + + abstract getState(): M + + abstract getViewHolderClass(): ViewHolderClass + + getViewModel() { return this.vm } build(root: Group): void { - this.vm = new (this.getVMClass())(this.getModel(), this.getViewHolder()) - this.vm.build(root) + this.vh = new (this.getViewHolderClass()) + this.vm = new (this.getViewModelClass())(this.getState(), this.vh) + this.vm.attach(root) } } -export abstract class ViewModel { - private model: M - private listeners: Function[] = [] - private viewHolder: V - - constructor(obj: M, v: V) { - this.model = listen(obj, () => { - this.listeners.forEach(e => { - Reflect.apply(e, this.model, [this.model]) - }) - }) - this.viewHolder = v - } - - build(root: Group) { - this.viewHolder.build(root) - this.bind((data: M) => { - this.binding(this.viewHolder, data) - }) - } - - abstract binding(v: V, model: M): void - - getModel() { - return this.model - } - - bind(f: (data: M) => void) { - Reflect.apply(f, this.model, [this.model]) - this.listeners.push(f) - } -} \ No newline at end of file From 81eb27843a5fbfefaafd7ab27e078e8f366e952d Mon Sep 17 00:00:00 2001 From: "pengfei.zhou" Date: Fri, 25 Oct 2019 03:15:23 +0800 Subject: [PATCH 2/3] modify demo --- demo/src/Counter.ts | 162 +++++++++++---------- demo/src/Snake.ts | 273 ++++++++++++++++++++---------------- js-framework/src/vm/mvvm.ts | 17 ++- 3 files changed, 248 insertions(+), 204 deletions(-) diff --git a/demo/src/Counter.ts b/demo/src/Counter.ts index 5da84dc6..33b1a2f8 100644 --- a/demo/src/Counter.ts +++ b/demo/src/Counter.ts @@ -1,71 +1,83 @@ -import { Image, ViewHolder, VMPanel, ViewModel, Gravity, NativeCall, Text, Color, VLayout, log, logw, loge, Group, LayoutSpec, } from "doric" +import { vlayout, Image, ViewHolder, VMPanel, ViewModel, Gravity, NativeCall, Text, Color, log, logw, loge, Group, LayoutSpec, } from "doric" interface CountModel { count: number } -class CounterView extends ViewHolder { - number = new Text - counter = new Text +class CounterView extends ViewHolder { + + number!: Text + counter!: Text build(root: Group) { - const vlayout = new VLayout - vlayout.width = 200 - vlayout.height = 200 - vlayout.gravity = new Gravity().center() - this.number.textSize = 40 - this.number.layoutConfig = { - alignment: new Gravity().center() - } - this.counter = new Text - this.counter.text = "点击计数" - this.counter.border = { - width: 1, - color: Color.parse('#000000'), - } - this.counter.textSize = 20 - this.counter.corners = 5 - vlayout.space = 20 - vlayout.layoutConfig = { - alignment: new Gravity().center() - } - vlayout.border = { - width: 1, - color: Color.parse("#000000"), - } - this.counter.shadow = { - color: Color.parse("#00ff00"), - opacity: 0.5, - radius: 20, - offsetX: 10, - offsetY: 10, - } - vlayout.shadow = { - color: Color.parse("#ffff00"), - opacity: 0.5, - radius: 20, - offsetX: 10, - offsetY: 10, - } - vlayout.corners = 20 - vlayout.addChild(this.number) - vlayout.addChild(this.counter) - // root.bgColor = Color.parse('#00ff00') - vlayout.bgColor = Color.parse('#ff00ff') - root.addChild(vlayout) - const iv = new Image - // iv.width = iv.height = 100 - iv.imageUrl = "https://misc.aotu.io/ONE-SUNDAY/SteamEngine.png" - //iv.bgColor = Color.parse('#00ff00') - iv.layoutConfig = { - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - root.addChild(iv) + root.addChild(vlayout([ + () => { + return (new Text).also(it => { + it.textSize = 40 + it.layoutConfig = { + alignment: new Gravity().center() + } + this.number = it + }) + }, + () => { + return (new Text).also(it => { + it.text = "点击计数" + it.textSize = 20 + + it.border = { + width: 1, + color: Color.parse('#000000'), + } + it.corners = 5 + + it.layoutConfig = { + alignment: new Gravity().center() + } + it.shadow = { + color: Color.parse("#00ff00"), + opacity: 0.5, + radius: 20, + offsetX: 10, + offsetY: 10, + } + this.counter = it + }) + }, + ]).also(it => { + it.width = 200 + it.height = 200 + it.space = 20 + it.gravity = new Gravity().center() + it.layoutConfig = { + alignment: new Gravity().center() + } + it.border = { + width: 1, + color: Color.parse("#000000"), + } + it.shadow = { + color: Color.parse("#ffff00"), + opacity: 0.5, + radius: 20, + offsetX: 10, + offsetY: 10, + } + it.corners = 20 + it.bgColor = Color.parse('#ff00ff') + })) + + root.addChild((new Image).also(iv => { + iv.imageUrl = "https://misc.aotu.io/ONE-SUNDAY/SteamEngine.png" + iv.layoutConfig = { + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + })) } - setNumber(n: number) { - this.number.text = n.toString() + bind(state: CountModel) { + this.number.text = `${state.count}` } setCounter(v: Function) { @@ -74,35 +86,33 @@ class CounterView extends ViewHolder { } class CounterVM extends ViewModel { - - binding(v: CounterView, model: CountModel): void { - v.setNumber(model.count) - v.setCounter(() => { - this.getModel().count++ - }) + onAttached(s: CountModel, vh: CounterView): void { + vh.counter.onClick = () => { + this.updateState(state => { + state.count++ + }) + } } } @Entry -class MyPage extends VMPanel{ +class MyPage extends VMPanel{ - getVMClass() { + + getViewHolderClass() { + return CounterView + } + + getViewModelClass() { return CounterVM } - getModel() { + getState(): CountModel { return { - count: 0, - add: function () { - this.count++ - }, + count: 0 } } - getViewHolder() { - return new CounterView - } - @NativeCall log() { diff --git a/demo/src/Snake.ts b/demo/src/Snake.ts index 617d0306..1292cd99 100644 --- a/demo/src/Snake.ts +++ b/demo/src/Snake.ts @@ -1,4 +1,4 @@ -import { loge, log, ViewHolder, Stack, ViewModel, Gravity, Text, Color, HLayout, VLayout, Group, VMPanel, LayoutSpec } from "doric"; +import { loge, log, ViewHolder, Stack, ViewModel, Gravity, Text, Color, HLayout, VLayout, Group, VMPanel, LayoutSpec, vlayout, hlayout } from "doric"; type SnakeNode = { x: number @@ -134,7 +134,8 @@ class SnakeModel { } } -class SnakeView extends ViewHolder { +class SnakeView extends ViewHolder { + panel: Stack = new Stack start: Text = new Text up?: Text @@ -144,79 +145,109 @@ class SnakeView extends ViewHolder { build(root: Group): void { root.bgColor = Color.parse('#000000') - const vlayout = new VLayout - const title = new Text - title.text = "Snake" - title.textSize = 20 - title.textColor = Color.parse("#ffffff") - title.layoutConfig = { - alignment: new Gravity().centerX(), - margin: { - top: 20 + vlayout([ + () => { + return (new Text).also(title => { + title.text = "Snake" + title.textSize = 20 + title.textColor = Color.parse("#ffffff") + title.layoutConfig = { + alignment: new Gravity().centerX(), + margin: { + top: 20 + }, + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + }) }, - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - vlayout.space = 20 - vlayout.layoutConfig = { - alignment: new Gravity().centerX().top(), - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - this.panel.bgColor = Color.parse('#00ff00') - vlayout.addChild(title) - vlayout.addChild(this.panel) - root.addChild(vlayout) + () => { + return (new Stack).also(panel => { + panel.bgColor = Color.parse('#00ff00') - const hlayout = new HLayout - hlayout.layoutConfig = { - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - hlayout.addChild(this.start.also( - it => { - it.text = "Start" - it.textSize = 30 - it.textColor = Color.parse("#ffffff") - it.layoutConfig = { - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - })) - vlayout.addChild(hlayout) - - - this.up = this.buildController("↑") - this.down = this.buildController("↓") - this.left = this.buildController("←") - this.right = this.buildController("→") - - const controlArea = new VLayout - controlArea.gravity = new Gravity().centerX() - controlArea.space = 10 - controlArea.layoutConfig = { - alignment: new Gravity().centerX(), - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - const line1 = new HLayout - const line2 = new HLayout - line2.space = 10 - line1.addChild(this.up) - line2.addChild(this.left) - line2.addChild(this.down) - line2.addChild(this.right) - line1.layoutConfig = { - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - line2.layoutConfig = { - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, - } - controlArea.addChild(line1) - controlArea.addChild(line2) - vlayout.addChild(controlArea) + }) + }, + () => { + return hlayout([ + () => { + return (new Text).also(it => { + it.text = "Start" + it.textSize = 30 + it.textColor = Color.parse("#ffffff") + it.layoutConfig = { + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + this.start = it + }) + }, + ]).also(it => { + it.layoutConfig = { + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + }) + }, + () => { + return vlayout([ + () => { + return hlayout([ + () => { + return this.buildController("↑").also(it => { + this.up = it + }) + } + ]).also(it => { + it.layoutConfig = { + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + }) + }, + () => { + return hlayout([ + () => { + return this.buildController("←").also(it => { + this.left = it + }) + }, + () => { + return this.buildController("↓").also(it => { + this.down = it + }) + }, + () => { + return this.buildController("→").also(it => { + this.right = it + }) + }, + ]).also(it => { + it.layoutConfig = { + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + it.space = 10 + }) + }, + ]) + .also(controlArea => { + controlArea.gravity = new Gravity().centerX() + controlArea.space = 10 + controlArea.layoutConfig = { + alignment: new Gravity().centerX(), + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + }) + } + ]).also(it => { + it.space = 20 + it.layoutConfig = { + alignment: new Gravity().centerX().top(), + widthSpec: LayoutSpec.WRAP_CONTENT, + heightSpec: LayoutSpec.WRAP_CONTENT, + } + }) } buildController(text: string) { @@ -228,19 +259,54 @@ class SnakeView extends ViewHolder { ret.textAlignment = new Gravity().center() return ret } + + bind(state: SnakeModel): void { + this.panel.width = state.width * 10 + this.panel.height = state.height * 10 + let node: SnakeNode | undefined = state.head + let nodes: SnakeNode[] = [] + while (node != undefined) { + nodes.push(node) + node = node.next + } + nodes.push(state.food) + nodes.forEach((e, index) => { + + let item = this.panel.children[index] + if (item === undefined) { + item = new Stack + item.width = item.height = 10 + this.panel.addChild(item) + } + if (index === nodes.length - 1) { + item.bgColor = Color.parse('#ffff00') + } else { + item.bgColor = Color.parse('#ff0000') + } + item.x = e.x * 10 + item.y = e.y * 10 + }) + + if (nodes.length < this.panel.children.length) { + this.panel.children.length = nodes.length + } + } } class SnakeVM extends ViewModel{ - timerId?: any start = () => { if (this.timerId !== undefined) { clearInterval(this.timerId) } - this.getModel().reset() + this.updateState(it => it.reset()) this.timerId = setInterval(() => { - this.getModel().step() + this.updateState(it => it.step()) + if (this.getState().state === State.fail) { + loge('Game Over') + this.stop() + } }, 500) } @@ -252,56 +318,23 @@ class SnakeVM extends ViewModel{ } left = () => { - this.getModel().direction = Direction.left + this.updateState(it => it.direction = Direction.left) } right = () => { - this.getModel().direction = Direction.right + this.updateState(it => it.direction = Direction.right) } up = () => { - this.getModel().direction = Direction.up + this.updateState(it => it.direction = Direction.up) } down = () => { - this.getModel().direction = Direction.down + this.updateState(it => it.direction = Direction.down) } - binding(v: SnakeView, model: SnakeModel) { - if (model.state === State.fail) { - loge('Game Over') - this.stop() - } + onAttached(state: SnakeModel, v: SnakeView): void { v.start.onClick = this.start - v.panel.width = model.width * 10 - v.panel.height = model.height * 10 - let node: SnakeNode | undefined = model.head - let nodes: SnakeNode[] = [] - while (node != undefined) { - nodes.push(node) - node = node.next - } - nodes.push(model.food) - nodes.forEach((e, index) => { - - let item = v.panel.children[index] - if (item === undefined) { - item = new Stack - item.width = item.height = 10 - v.panel.addChild(item) - } - if (index === nodes.length - 1) { - item.bgColor = Color.parse('#ffff00') - } else { - item.bgColor = Color.parse('#ff0000') - } - item.x = e.x * 10 - item.y = e.y * 10 - }) - - if (nodes.length < v.panel.children.length) { - v.panel.children.length = nodes.length - } if (v.left) { v.left.onClick = this.left } @@ -315,19 +348,17 @@ class SnakeVM extends ViewModel{ v.down.onClick = this.down } } + } @Entry class SnakePanel extends VMPanel{ - - getVMClass() { + getViewModelClass() { return SnakeVM } - - getModel() { + getState(): SnakeModel { return new SnakeModel(35, 35) } - - getViewHolder() { - return new SnakeView + getViewHolderClass() { + return SnakeView } } \ No newline at end of file diff --git a/js-framework/src/vm/mvvm.ts b/js-framework/src/vm/mvvm.ts index e31b4576..da3a85d1 100644 --- a/js-framework/src/vm/mvvm.ts +++ b/js-framework/src/vm/mvvm.ts @@ -43,22 +43,25 @@ export abstract class ViewModel> { attach(view: Group) { this.viewHolder.build(view) + this.viewHolder.bind(this.state) } + + abstract onAttached(state: M, vh: V): void } -export type ViewModelClass = new (m: M, v: ViewHolder) => ViewModel> +export type ViewModelClass> = new (m: M, v: V) => ViewModel -export type ViewHolderClass = new () => ViewHolder +export type ViewHolderClass = new () => V -export abstract class VMPanel extends Panel { +export abstract class VMPanel> extends Panel { - private vm?: ViewModel> - private vh?: ViewHolder + private vm?: ViewModel + private vh?: V - abstract getViewModelClass(): ViewModelClass + abstract getViewModelClass(): ViewModelClass abstract getState(): M - abstract getViewHolderClass(): ViewHolderClass + abstract getViewHolderClass(): ViewHolderClass getViewModel() { return this.vm From 182e6c9b748c192acc4732a24717ab6dbb085384 Mon Sep 17 00:00:00 2001 From: "pengfei.zhou" Date: Fri, 25 Oct 2019 14:13:35 +0800 Subject: [PATCH 3/3] feat:adjust mvvm api --- demo/src/Counter.ts | 2 +- demo/src/Snake.ts | 34 ++++++++++++++-------------------- js-framework/src/ui/view.ts | 3 +++ js-framework/src/vm/mvvm.ts | 1 + 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/demo/src/Counter.ts b/demo/src/Counter.ts index 33b1a2f8..725c2e38 100644 --- a/demo/src/Counter.ts +++ b/demo/src/Counter.ts @@ -96,7 +96,7 @@ class CounterVM extends ViewModel { } @Entry -class MyPage extends VMPanel{ +class MyPage extends VMPanel{ getViewHolderClass() { diff --git a/demo/src/Snake.ts b/demo/src/Snake.ts index 1292cd99..f32c4bd3 100644 --- a/demo/src/Snake.ts +++ b/demo/src/Snake.ts @@ -1,4 +1,4 @@ -import { loge, log, ViewHolder, Stack, ViewModel, Gravity, Text, Color, HLayout, VLayout, Group, VMPanel, LayoutSpec, vlayout, hlayout } from "doric"; +import { loge, log, ViewHolder, Stack, ViewModel, Gravity, Text, Color, HLayout, VLayout, Group, VMPanel, LayoutSpec, vlayout, hlayout, takeNonNull } from "doric"; type SnakeNode = { x: number @@ -136,8 +136,8 @@ class SnakeModel { class SnakeView extends ViewHolder { - panel: Stack = new Stack - start: Text = new Text + panel!: Stack + start?: Text up?: Text down?: Text left?: Text @@ -164,7 +164,7 @@ class SnakeView extends ViewHolder { () => { return (new Stack).also(panel => { panel.bgColor = Color.parse('#00ff00') - + this.panel = panel }) }, () => { @@ -244,10 +244,11 @@ class SnakeView extends ViewHolder { it.space = 20 it.layoutConfig = { alignment: new Gravity().centerX().top(), - widthSpec: LayoutSpec.WRAP_CONTENT, - heightSpec: LayoutSpec.WRAP_CONTENT, + widthSpec: LayoutSpec.AT_MOST, + heightSpec: LayoutSpec.AT_MOST, } - }) + it.gravity = new Gravity().centerX() + }).in(root) } buildController(text: string) { @@ -261,6 +262,7 @@ class SnakeView extends ViewHolder { } bind(state: SnakeModel): void { + log('build', state) this.panel.width = state.width * 10 this.panel.height = state.height * 10 let node: SnakeNode | undefined = state.head @@ -334,19 +336,11 @@ class SnakeVM extends ViewModel{ } onAttached(state: SnakeModel, v: SnakeView): void { - v.start.onClick = this.start - if (v.left) { - v.left.onClick = this.left - } - if (v.right) { - v.right.onClick = this.right - } - if (v.up) { - v.up.onClick = this.up - } - if (v.down) { - v.down.onClick = this.down - } + takeNonNull(v.start)(it => it.onClick = this.start) + takeNonNull(v.left)(it => it.onClick = this.left) + takeNonNull(v.right)(it => it.onClick = this.right) + takeNonNull(v.up)(it => it.onClick = this.up) + takeNonNull(v.down)(it => it.onClick = this.down) } } diff --git a/js-framework/src/ui/view.ts b/js-framework/src/ui/view.ts index b222309e..379feb3f 100644 --- a/js-framework/src/ui/view.ts +++ b/js-framework/src/ui/view.ts @@ -230,6 +230,9 @@ export abstract class View implements Modeling { block(this) return this } + in(group: Group) { + group.addChild(this) + } } export interface StackConfig extends LayoutConfig { diff --git a/js-framework/src/vm/mvvm.ts b/js-framework/src/vm/mvvm.ts index da3a85d1..24eb05d9 100644 --- a/js-framework/src/vm/mvvm.ts +++ b/js-framework/src/vm/mvvm.ts @@ -44,6 +44,7 @@ export abstract class ViewModel> { attach(view: Group) { this.viewHolder.build(view) this.viewHolder.bind(this.state) + this.onAttached(this.state, this.viewHolder) } abstract onAttached(state: M, vh: V): void