Skip to content

Commit

Permalink
Add basic API for custom components (#2)
Browse files Browse the repository at this point in the history
## Description

This PR adds a simple API for creating custom components:
- components with type `Custom` are treated like `Stack` when measured, and its children are positioned at the top-start.
- `Custom` function accepts additional argument with custom config
- custom config may be used to store data between updates
- custom config may additionally contain hooks for specific moments:
  -  `dropHandler` - invoked when the node is dropped from the tree
  - `createView` - invoked when the view enters the view hierarchy, when implemented it must return a reference to the created view
  - `overrideViewProps` - invoked after the ViewManager finishes setting up the newly created view, it may be used to overwrite them
  - `updateView` - invoked during update, receives previous and the next node
  - `deleteView` - invoked when the view leaves the view hierarchy, when implemented it must delete the created view (note that this method may be called without calling `dropHandler` to reorder views)
  • Loading branch information
j-piasecki authored Sep 2, 2022
1 parent b28f3df commit beaa23c
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 7 deletions.
1 change: 1 addition & 0 deletions @zapp/core/src/NodeType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export enum NodeType {
Root = 'root',
Recomposing = 'recomposing',

Custom = 'custom',
Screen = 'screen',
Stack = 'stack',
Column = 'column',
Expand Down
3 changes: 3 additions & 0 deletions @zapp/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export type { ConfigBuilderArg } from './working_tree/props/Config.js'

export { remember } from './working_tree/effects/remember.js'
export { RememberedMutableValue } from './working_tree/effects/RememberedMutableValue.js'
export { sideEffect } from './working_tree/effects/sideEffect.js'
export { Custom } from './working_tree/views/Custom.js'
export { Stack } from './working_tree/views/Stack.js'
export { Column } from './working_tree/views/Column.js'
export { Row } from './working_tree/views/Row.js'
Expand Down
12 changes: 8 additions & 4 deletions @zapp/core/src/renderer/LayoutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ export class LayoutManager {
if (node.layout.height === -1 && maxHeight !== -1 && node.config.fillHeight === undefined) {
node.layout.height = Math.max(node.layout.height, maxHeight + verticalPadding)
}
} else if (node.type === NodeType.Stack) {
// column stacks its children on top of each other so we want both its width and height to match
// the widest and highest child respectively
} else if (node.type === NodeType.Stack || node.type === NodeType.Custom) {
// stack stacks its children on top of each other so we want both its width and height to match
// the widest and highest child respectively, we also treat custom views in the same way as stack
let maxWidth = -1
let maxHeight = -1

Expand Down Expand Up @@ -272,7 +272,11 @@ export class LayoutManager {
this.positionStack(node)
} else {
for (const child of node.children) {
child.layout.x = node.layout.x + (node.config.padding?.start ?? 0)
child.layout.x =
node.layout.x +
(this.viewManager.isRTL()
? node.layout.width - child.layout.width - (node.config.padding?.start ?? 0)
: node.config.padding?.start ?? 0)
child.layout.y = node.layout.y + (node.config.padding?.end ?? 0)
}
}
Expand Down
3 changes: 3 additions & 0 deletions @zapp/core/src/renderer/Renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NodeType } from '../NodeType.js'
import { ConfigType } from '../working_tree/props/types.js'
import { ViewNode } from '../working_tree/ViewNode.js'
import { CustomViewProps } from '../working_tree/views/Custom.js'
import { EventManager } from './EventManager.js'
import { LayoutManager } from './LayoutManager.js'
import { ViewManager } from './ViewManager.js'
Expand All @@ -25,6 +26,7 @@ export interface RenderNode {
view: unknown
zIndex: number
layout: Layout
customViewProps?: CustomViewProps
}

export abstract class Renderer {
Expand Down Expand Up @@ -232,6 +234,7 @@ export abstract class Renderer {
view: null,
zIndex: -1,
layout: Renderer.createLayout(),
customViewProps: node.customViewProps,
}

for (const child of node.children) {
Expand Down
20 changes: 19 additions & 1 deletion @zapp/core/src/working_tree/ViewNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { EffectNode } from './EffectNode.js'
import { RememberNode } from './RememberNode.js'
import { WorkingNode, WorkingNodeProps } from './WorkingNode.js'
import { WorkingTree } from './WorkingTree.js'
import { findRelativePath } from '../utils.js'
import { CustomViewProps } from './views/Custom.js'

export interface ViewNodeProps extends WorkingNodeProps {
body: () => void
Expand All @@ -20,6 +22,9 @@ export class ViewNode extends WorkingNode {
public nextActionId: number
public rememberedContext: ViewNode | undefined

// custom view properties
public customViewProps?: CustomViewProps

constructor(props: ViewNodeProps) {
super(props)

Expand All @@ -30,12 +35,13 @@ export class ViewNode extends WorkingNode {
this.nextActionId = 0
}

public create(props: ViewNodeProps) {
public create(props: ViewNodeProps, customViewProps?: CustomViewProps) {
const result = new ViewNode(props)

// new view nodes may only be created inside another view node
const currentView = WorkingTree.current as ViewNode

result.customViewProps = customViewProps
result.parent = currentView.override ?? WorkingTree.current
result.rememberedContext = currentView.rememberedContext
result.path = this.path.concat(this.id)
Expand Down Expand Up @@ -81,6 +87,18 @@ export class ViewNode extends WorkingNode {
public override drop(newSubtreeRoot: WorkingNode): void {
super.drop(newSubtreeRoot)

if (this.type === NodeType.Custom && this.customViewProps?.dropHandler !== undefined) {
const thisPath = this.path.concat(this.id)
const relativePath = findRelativePath(thisPath, newSubtreeRoot.path)

if (relativePath !== null) {
const nodeAtPath = newSubtreeRoot.getNodeFromPath(relativePath)
if (nodeAtPath === null) {
this.customViewProps.dropHandler()
}
}
}

for (const child of this.children) {
child.drop(newSubtreeRoot)
}
Expand Down
3 changes: 3 additions & 0 deletions @zapp/core/src/working_tree/props/Config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { RequireSome } from '../../utils'
import { ConfigType, PointerData } from './types'

export function Config(id: string) {
return new ConfigBuilder(id)
}

export type ConfigBuilderArg = RequireSome<ConfigBuilder, 'build'>

export class ConfigBuilder {
protected config: ConfigType

Expand Down
59 changes: 59 additions & 0 deletions @zapp/core/src/working_tree/views/Custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { RequireSome } from '../../utils.js'
import { ViewNode } from '../ViewNode.js'
import { WorkingTree } from '../WorkingTree.js'
import { NodeType } from '../../NodeType.js'
import { ConfigBuilder } from '../props/Config.js'
import { RenderNode } from '../../renderer/Renderer.js'

export interface CustomViewProps extends Record<string, unknown> {
/**
* invoked when the node is dropped from the tree
*/
dropHandler?: () => void

/**
* invoked when the view enters the view hierarchy, when implemented itmust return a
* reference to the created view
*/
createView?: (config: RenderNode) => unknown

/**
* invoked after the ViewManager finishes setting up the newly created view, it may
* be used to overwrite them
*/
overrideViewProps?: (config: RenderNode) => void

/**
* invoked during update, receives previous and the next node
*/
updateView?: (previous: RenderNode, next: RenderNode) => void

/**
* invoked when the view leaves the view hierarchy, when implemented it must delete the created
* view (note that this method may be called without calling dropHandler to reorder views)
*/
deleteView?: (config: RenderNode) => void
}

export function Custom(
configBuilder: RequireSome<ConfigBuilder, 'build'>,
customViewProps: CustomViewProps,
body: () => void
) {
const config = configBuilder.build()
const current = WorkingTree.current as ViewNode

const context = current.create(
{
id: config.id,
type: NodeType.Custom,
config: config,
body: body,
},
customViewProps
)

current.children.push(context)

WorkingTree.withContext(context, body)
}
19 changes: 17 additions & 2 deletions @zapp/web/src/WebViewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ export class WebViewManager extends ViewManager {
}

createView(node: RenderNode): HTMLElement {
const view = document.createElement('div')
const view =
node.customViewProps?.createView !== undefined
? (node.customViewProps.createView(node) as HTMLElement)
: document.createElement('div')

view.id = node.id
view.style.position = 'absolute'
Expand Down Expand Up @@ -118,6 +121,10 @@ export class WebViewManager extends ViewManager {
view.addEventListener('pointerout', handler)
}

if (node.customViewProps?.overrideViewProps !== undefined) {
node.customViewProps.overrideViewProps(node)
}

document.getElementsByTagName('body')[0].appendChild(view)
console.log('create', node.id)

Expand All @@ -126,7 +133,11 @@ export class WebViewManager extends ViewManager {

dropView(node: RenderNode): void {
const view = node.view as HTMLElement
view?.remove()
if (node.customViewProps?.deleteView !== undefined) {
node.customViewProps.deleteView(node)
} else {
view?.remove()
}

this.eventListenrs.delete(`${node.id}#'pointerdown'`)
this.eventListenrs.delete(`${node.id}#'pointermove'`)
Expand Down Expand Up @@ -183,6 +194,10 @@ export class WebViewManager extends ViewManager {
view.removeEventListener('pointerout', this.eventListenrs.get(`${next.id}#pointerleave`)!)
this.eventListenrs.delete(`${next.id}#pointerleave`)
}

if (next.customViewProps?.updateView !== undefined) {
next.customViewProps.updateView(previous, next)
}
}

measureText(
Expand Down

0 comments on commit beaa23c

Please sign in to comment.