Skip to content

Commit 4f4fba0

Browse files
authored
feat: inject components into the first visible context (DAP-4727) (#12)
* feat: introduce visibility flag in context nodes (DAP-4727) * feat: introduce `isFirstTargetOnly` flag to inject components into the first context only (DAP-4727)
1 parent c506367 commit 4f4fba0

File tree

13 files changed

+233
-51
lines changed

13 files changed

+233
-51
lines changed

libs/core/src/adapters/dynamic-html-adapter.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export class DynamicHtmlAdapter implements IAdapter {
1111
public namespace: string
1212
public context: IContextNode
1313

14-
#observerByElement: Map<HTMLElement, MutationObserver> = new Map()
14+
#mutationObserverByElement: Map<HTMLElement, MutationObserver> = new Map()
15+
#intersectionObserverByElement: Map<HTMLElement, IntersectionObserver> = new Map()
1516
#contextByElement: Map<HTMLElement, IContextNode> = new Map()
1617

1718
#isStarted = false // ToDo: find another way to check if adapter is started
@@ -27,7 +28,7 @@ export class DynamicHtmlAdapter implements IAdapter {
2728
}
2829

2930
start() {
30-
this.#observerByElement.forEach((observer, element) => {
31+
this.#mutationObserverByElement.forEach((observer, element) => {
3132
observer.observe(element, {
3233
attributes: true,
3334
childList: true,
@@ -38,12 +39,18 @@ export class DynamicHtmlAdapter implements IAdapter {
3839
// initial parsing without waiting for mutations in the DOM
3940
this._handleMutations(element, this.#contextByElement.get(element)!)
4041
})
42+
43+
this.#intersectionObserverByElement.forEach((observer, element) => {
44+
observer.observe(element)
45+
})
46+
4147
this.#isStarted = true
4248
}
4349

4450
stop() {
4551
this.#isStarted = false
46-
this.#observerByElement.forEach((observer) => observer.disconnect())
52+
this.#mutationObserverByElement.forEach((observer) => observer.disconnect())
53+
this.#intersectionObserverByElement.forEach((observer) => observer.disconnect())
4754
}
4855

4956
_tryCreateContextForElement(element: HTMLElement, contextName: string): IContextNode | null
@@ -76,27 +83,47 @@ export class DynamicHtmlAdapter implements IAdapter {
7683
element
7784
)
7885

79-
const observer = new MutationObserver((mutations, observer) => {
86+
const mutationObserver = new MutationObserver((mutations, observer) => {
8087
this._handleMutations(element, context)
8188

8289
if (this.parser.shouldParseShadowDom) {
8390
this._observeShadowRoots(mutations, observer)
8491
}
8592
})
8693

87-
this.#observerByElement.set(element, observer)
88-
this.#contextByElement.set(element, context)
94+
this.#mutationObserverByElement.set(element, mutationObserver)
8995

9096
// ToDo: duplicate code
9197
if (this.#isStarted) {
92-
observer.observe(element, {
98+
mutationObserver.observe(element, {
9399
attributes: true,
94100
childList: true,
95101
subtree: true,
96102
characterData: true,
97103
})
98104
}
99105

106+
// Only L2 contexts
107+
if (element !== this.element) {
108+
const intersectionObserver = new IntersectionObserver(
109+
([entry]) => {
110+
this.treeBuilder.updateVisibility(context, entry.isIntersecting)
111+
},
112+
{
113+
threshold: 1, // isIntersecting is true when 100% of the context element is in viewport
114+
}
115+
)
116+
117+
this.#intersectionObserverByElement.set(element, intersectionObserver)
118+
119+
// ToDo: duplicate code
120+
if (this.#isStarted) {
121+
intersectionObserver.observe(element)
122+
}
123+
}
124+
125+
this.#contextByElement.set(element, context)
126+
100127
return context
101128
}
102129

@@ -142,8 +169,10 @@ export class DynamicHtmlAdapter implements IAdapter {
142169
if (!childElementsSet.has(element) && context.parentNode === parentContext) {
143170
this.treeBuilder.removeChild(parentContext, context)
144171
this.#contextByElement.delete(element)
145-
this.#observerByElement.get(element)?.disconnect()
146-
this.#observerByElement.delete(element)
172+
this.#mutationObserverByElement.get(element)?.disconnect()
173+
this.#mutationObserverByElement.delete(element)
174+
this.#intersectionObserverByElement.get(element)?.disconnect()
175+
this.#intersectionObserverByElement.delete(element)
147176
}
148177
}
149178
}

libs/core/src/tree/pure-tree/pure-context-node.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class PureContextNode implements IContextNode {
1111
public element: HTMLElement | null = null
1212

1313
#parsedContext: any = {}
14+
#isVisible = false
1415
#eventEmitter = new EventEmitter<TreeNodeEvents>() // ToDo: implement event bubbling?
1516

1617
public get parsedContext() {
@@ -22,6 +23,15 @@ export class PureContextNode implements IContextNode {
2223
this.#eventEmitter.emit('contextChanged', {})
2324
}
2425

26+
public get isVisible() {
27+
return this.#isVisible
28+
}
29+
30+
public set isVisible(value: boolean) {
31+
this.#isVisible = value
32+
this.#eventEmitter.emit('visibilityChanged', {})
33+
}
34+
2535
constructor(
2636
namespace: string,
2737
contextType: string,
@@ -43,7 +53,7 @@ export class PureContextNode implements IContextNode {
4353
child.parentNode = null
4454
this.children = this.children.filter((c) => c !== child)
4555
this.#eventEmitter.emit('childContextRemoved', { child })
46-
56+
4757
// ToDo: remove children of removed context?
4858
}
4959

libs/core/src/tree/pure-tree/pure-tree-builder.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export class PureTreeBuilder implements ITreeBuilder {
7171
})
7272
}
7373

74+
updateVisibility(context: IContextNode, isVisible: boolean): void {
75+
if (context.isVisible !== isVisible) {
76+
context.isVisible = isVisible
77+
}
78+
}
79+
7480
clear() {
7581
// ToDo: move to engine, it's not a core responsibility
7682
this.root = this.createNode(DappletsEngineNs, 'website') // default ns

libs/core/src/tree/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { InsertionPoint } from '../parsers/interface'
33

44
export type TreeNodeEvents = {
55
contextChanged: {}
6+
visibilityChanged: {}
67
childContextAdded: { child: IContextNode }
78
childContextRemoved: { child: IContextNode }
89
insertionPointAdded: { insertionPoint: InsertionPointWithElement }
@@ -23,6 +24,7 @@ export interface IContextNode {
2324
namespace: string
2425
parentNode: IContextNode | null // ToDo: rename to parent
2526
element: HTMLElement | null
27+
isVisible: boolean
2628

2729
parsedContext: ParsedContext // ToDo: rename to parsed
2830
insPoints: InsertionPointWithElement[]
@@ -46,6 +48,7 @@ export interface ITreeBuilder {
4648
removeChild(parent: IContextNode, child: IContextNode): void
4749
updateParsedContext(context: IContextNode, parsedContext: any): void
4850
updateInsertionPoints(context: IContextNode, insPoints: InsertionPointWithElement[]): void
51+
updateVisibility(context: IContextNode, isVisible: boolean): void
4952
createNode(
5053
namespace: string | null,
5154
contextType: string,

libs/engine/src/app/common/transferable-context.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { IContextNode } from "@mweb/core"
1+
import { IContextNode } from '@mweb/core'
22

33
export interface TransferableContext {
44
namespace: string
55
type: string
66
id: string | null
77
parsed: any
88
parent: TransferableContext | null
9+
isVisible: boolean
910
}
1011

1112
// ToDo: reuse in ContextPicker
@@ -15,4 +16,5 @@ export const buildTransferableContext = (context: IContextNode): TransferableCon
1516
id: context.id,
1617
parsed: context.parsedContext,
1718
parent: context.parentNode ? buildTransferableContext(context.parentNode) : null,
19+
isVisible: context.isVisible,
1820
})

libs/engine/src/app/components/context-manager.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC, useCallback, useMemo, useRef, useState } from 'react'
1+
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { ContextPortal } from '@mweb/react'
33
import { IContextNode, InsertionPointWithElement } from '@mweb/core'
44
import { useEngine } from '../contexts/engine-context'
@@ -23,10 +23,6 @@ import { Portal } from '../contexts/engine-context/engine-context'
2323
import { Target } from '../services/target/target.entity'
2424
import { filterAndDiscriminate } from '../common/filter-and-discriminate'
2525

26-
const getRootContext = (context: IContextNode): IContextNode => {
27-
return context.parentNode ? getRootContext(context.parentNode) : context
28-
}
29-
3026
interface WidgetProps {
3127
context: TransferableContext
3228
link?: {
@@ -98,7 +94,19 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE
9894
return Array.from(portals.values())
9995
.filter(({ target }) => TargetService.isTargetMet(target, context))
10096
.sort((a, b) => (b.key > a.key ? 1 : -1))
101-
}, [portals, context])
97+
}, [portals, context.parsedContext, context.isVisible])
98+
99+
useEffect(() => {
100+
portalComponents.forEach(({ onContextStarted }) => {
101+
onContextStarted?.(context)
102+
})
103+
104+
return () => {
105+
portalComponents.forEach(({ onContextFinished }) => {
106+
onContextFinished?.(context)
107+
})
108+
}
109+
}, [portalComponents])
102110

103111
const [materializedComponents, nonMaterializedComponents] = useMemo(() => {
104112
return filterAndDiscriminate(portalComponents, (portal) => portal.inMemory)
@@ -129,7 +137,7 @@ const ContextHandler: FC<{ context: IContextNode; insPoints: InsertionPointWithE
129137

130138
const handleContextQuery = useCallback(
131139
(target: Target): TransferableContext | null => {
132-
const rootContext = getRootContext(context)
140+
const rootContext = TargetService.getRootContext(context)
133141
const foundContext = TargetService.findContextByTarget(target, rootContext)
134142
return foundContext ? buildTransferableContext(foundContext) : null
135143
},
@@ -495,6 +503,8 @@ const PortalRenderer: FC<{
495503
[target, context]
496504
)
497505

506+
if (!PortalComponent) return null
507+
498508
return (
499509
<InMemoryRenderer>
500510
<PortalComponent

libs/engine/src/app/contexts/engine-context/engine-context.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { createContext } from 'react'
22
import { BosRedirectMap } from '../../services/dev-server-service'
33
import { Target } from '../../services/target/target.entity'
44
import { TransferableContext } from '../../common/transferable-context'
5+
import { IContextNode } from '@mweb/core'
56

6-
export type InjectableTarget = Target & {
7+
export type InjectableTarget = (Target | TransferableContext) & {
78
injectTo?: string
89
}
910

@@ -14,10 +15,12 @@ export type PortalComponent = React.FC<{
1415
}>
1516

1617
export type Portal = {
17-
component: PortalComponent
18+
component?: PortalComponent
1819
target: InjectableTarget
1920
key: string
2021
inMemory: boolean
22+
onContextStarted?: (context: IContextNode) => void
23+
onContextFinished?: (context: IContextNode) => void
2124
}
2225

2326
export type EngineContextState = {

libs/engine/src/app/contexts/mutable-web-context/use-user-links.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { BosUserLink, UserLinkId } from '../../services/user-link/user-link.enti
44
import { useMutableWeb } from '.'
55
import { AppId } from '../../services/application/application.entity'
66

7+
// Reuse reference to empty array to avoid unnecessary re-renders
8+
const NoLinks: BosUserLink[] = []
9+
710
export const useUserLinks = (context: IContextNode) => {
811
const { engine, selectedMutation, activeApps } = useMutableWeb()
912
const [userLinks, setUserLinks] = useState<BosUserLink[]>([])
@@ -15,7 +18,7 @@ export const useUserLinks = (context: IContextNode) => {
1518
} else {
1619
return engine.userLinkService.getStaticLinksForApps(activeApps, context)
1720
}
18-
}, [engine, selectedMutation, activeApps, context])
21+
}, [engine, selectedMutation, activeApps, context.parsedContext, context.isVisible])
1922

2023
const fetchUserLinks = useCallback(async () => {
2124
if (!engine || !selectedMutation?.id) {
@@ -43,7 +46,9 @@ export const useUserLinks = (context: IContextNode) => {
4346
fetchUserLinks()
4447
}, [fetchUserLinks])
4548

46-
const links = useMemo(() => [...userLinks, ...staticLinks], [userLinks, staticLinks])
49+
const links = useMemo(() => {
50+
return userLinks.length || staticLinks.length ? [...userLinks, ...staticLinks] : NoLinks
51+
}, [userLinks, staticLinks])
4752

4853
const createUserLink = useCallback(
4954
async (appId: AppId) => {

libs/engine/src/app/services/target/target.entity.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export type Target = {
22
namespace: string
33
contextType: string
4+
isVisible?: boolean
45
if: Record<string, TargetCondition>
6+
limit?: number
57
parent?: Target
68
}
79

libs/engine/src/app/services/target/target.service.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1-
import { IContextNode, isDeepEqual } from '@mweb/core'
1+
import { IContextNode } from '@mweb/core'
22
import { TransferableContext } from '../../common/transferable-context'
33
import { ScalarType, TargetCondition, Target } from './target.entity'
44

55
export class TargetService {
6-
static findContextByTarget(target: Target, context: IContextNode): IContextNode | null {
6+
static *findContextsByTarget(
7+
target: Target | TransferableContext,
8+
context: IContextNode
9+
): Generator<IContextNode> {
710
if (this.isTargetMet(target, context)) {
8-
return context
11+
yield context
912
}
1013

1114
for (const child of context.children) {
12-
const found = this.findContextByTarget(target, child)
15+
yield* this.findContextsByTarget(target, child)
16+
}
17+
}
1318

14-
if (found) {
15-
return found
16-
}
19+
static findContextByTarget(
20+
target: Target | TransferableContext,
21+
context: IContextNode
22+
): IContextNode | null {
23+
for (const ctx of this.findContextsByTarget(target, context)) {
24+
return ctx
1725
}
1826

1927
return null
@@ -46,7 +54,12 @@ export class TargetService {
4654
return false
4755
}
4856

49-
// ToDo: disabled
57+
// for Target
58+
if ('isVisible' in target && target.isVisible !== context.isVisible) {
59+
return false
60+
}
61+
62+
// ToDo: disabled
5063
// for TransferableContext
5164
// if ('parsed' in target && !isDeepEqual(target.parsed, context.parsedContext)) {
5265
// return false
@@ -62,6 +75,10 @@ export class TargetService {
6275
return true
6376
}
6477

78+
static getRootContext(context: IContextNode): IContextNode {
79+
return context.parentNode ? this.getRootContext(context.parentNode) : context
80+
}
81+
6582
static _areConditionsMet(
6683
conditions: Record<string, TargetCondition>,
6784
values: Record<string, ScalarType>

0 commit comments

Comments
 (0)