Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tame-bears-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lynx-js/a2ui-reactlynx": patch
---

Fix `Column` template rendering so nested bindings keep the correct `dataContextPath`, and preserve the caller-provided context in `NodeRenderer` instead of overwriting it from the stored component snapshot.
16 changes: 12 additions & 4 deletions packages/genui/a2ui/src/catalog/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../../../styles/catalog/Button.css';
export interface ButtonProps extends GenericComponentProps {
child: string;
variant?: 'primary' | 'borderless';
isValid?: boolean;
/** v0.9 actions should use the `event` wrapper for server-dispatched clicks. */
action: {
event: {
Expand All @@ -25,10 +26,17 @@ export interface ButtonProps extends GenericComponentProps {
export function Button(
props: ButtonProps,
): import('@lynx-js/react').ReactNode {
const { action, child, surface, sendAction, variant = 'primary' } = props;
const {
action,
child,
isValid = true,
surface,
sendAction,
variant = 'primary',
} = props;

const handleClick = () => {
if (action) {
if (isValid && action) {
void sendAction?.(action as Record<string, unknown>);
}
};
Expand All @@ -39,8 +47,8 @@ export function Button(

return (
<view
className={`button button-${variant}`}
bindtap={handleClick}
className={`button button-${variant}${isValid ? '' : ' button-disabled'}`}
bindtap={isValid ? handleClick : undefined}
>
{childResource
? <A2UIRenderer resource={childResource} />
Expand Down
98 changes: 80 additions & 18 deletions packages/genui/a2ui/src/catalog/Column/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
// Copyright 2026 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { useMemo } from '@lynx-js/react';

import { NodeRenderer } from '../../react/A2UIRenderer.jsx';
import type { GenericComponentProps } from '../../store/types.js';
import { useDataBinding } from '../../react/useDataBinding.js';
import type {
ComponentInstance,
GenericComponentProps,
Surface,
} from '../../store/types.js';

import '../../../styles/catalog/Column.css';

const buildChild = (
surface: Surface,
childId: string,
dataContextPath: string | undefined,
childPath?: string,
key = childId,
): {
key: string;
component: ComponentInstance;
} | null => {
const child = surface.components.get(childId);
if (!child) return null;
let childWithContext = child;
if (childPath) {
childWithContext = { ...child, dataContextPath: childPath };
} else if (dataContextPath) {
childWithContext = { ...child, dataContextPath };
}
Comment thread
HuJean marked this conversation as resolved.
return {
key,
component: childWithContext,
};
};

/**
* @a2uiCatalog Column
*/
Expand All @@ -26,42 +57,73 @@ export interface ColumnProps extends GenericComponentProps {
export function Column(
props: ColumnProps,
): import('@lynx-js/react').ReactNode {
const children = props.children;
const surface = props.surface;
const dataContextPath = props.dataContextPath;
const justify = props.justify as string | undefined ?? 'start';
const align = props.align as string | undefined ?? 'stretch';
const explicitChildren = Array.isArray(children) ? children : [];
const {
children,
surface,
dataContextPath,
justify = 'start',
align = 'stretch',
} = props;

const isDynamic = children && !Array.isArray(children)
&& typeof children === 'object';
const template = isDynamic
? children
: undefined;

const [columnData, , fullPath] = useDataBinding<Record<string, unknown>[]>(
template ? { path: template.path } : undefined,
surface,
dataContextPath,
[],
);

const childList = useMemo(() => {
if (Array.isArray(children)) {
return children.map((childId: string) =>
buildChild(surface, childId, dataContextPath)
);
}
return (Array.isArray(columnData) ? columnData : []).map((item, index) => {
const key = item && typeof item === 'object' && 'key' in item
? String(item['key'])
: `${index}`;
const itemPath = `${fullPath}/${index}`;
return buildChild(
surface,
template?.componentId ?? '',
dataContextPath,
itemPath,
key,
);
});
}, [children, surface, dataContextPath, columnData, fullPath, template]);
Comment thread
HuJean marked this conversation as resolved.

return (
<view
className={`column alignment-${align} distribution-${justify}`}
>
{explicitChildren.map((childId: string) => {
const child = surface.components.get(childId);
if (!child) return null;
const childWithContext = dataContextPath
? { ...child, dataContextPath: dataContextPath }
: child;
const weight = (child as unknown as { weight?: number }).weight;
{childList.map((item) => {
if (!item) return null;
const weight = item.component.weight;
if (typeof weight === 'number' && weight > 0) {
return (
<view
key={childId}
key={item.key}
className={`column-weighted-item column-weighted-item-${weight}`}
style={{ flex: `${weight} ${weight} 0`, minHeight: 0 }}
>
<NodeRenderer
component={childWithContext}
component={item.component}
surface={surface}
/>
</view>
);
}
return (
<NodeRenderer
key={childId}
component={childWithContext}
key={item.key}
component={item.component}
surface={surface}
/>
);
Expand Down
24 changes: 16 additions & 8 deletions packages/genui/a2ui/src/react/A2UIRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,35 +182,43 @@ function NodeRendererImpl(
const component =
(latest.value as { component?: ComponentInstance } | undefined)?.component
?? initialComponent;
const effectiveComponent = initialComponent.dataContextPath !== undefined
&& component.dataContextPath !== initialComponent.dataContextPath
? { ...component, dataContextPath: initialComponent.dataContextPath }
: component;

useEffect(() => {
const tag = component.component;
const tag = effectiveComponent.component;
if (!catalog.has(tag) && !warnedTags.has(tag)) {
warnedTags.add(tag);
console.warn(`[a2ui] Component "${tag}" is not in the active catalog.`);
}
}, [component.component, catalog]);
}, [effectiveComponent.component, catalog]);

const [resolvedProps, setValue] = useResolvedProps(
component,
effectiveComponent,
surface,
component.dataContextPath,
effectiveComponent.dataContextPath,
);

const actionProps = useMemo(
() => ({
id: component.id!,
id: effectiveComponent.id!,
surfaceId: surface.surfaceId,
dataContext: component.dataContextPath,
dataContext: effectiveComponent.dataContextPath,
}),
[component.id, surface.surfaceId, component.dataContextPath],
[
effectiveComponent.id,
surface.surfaceId,
effectiveComponent.dataContextPath,
],
);
const { sendAction } = useAction(actionProps);

return (
<>
{buildNodeRecursive(
component,
effectiveComponent,
surface,
catalog as ReadonlyMap<
string,
Expand Down
28 changes: 21 additions & 7 deletions packages/genui/a2ui/styles/catalog/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,44 @@
gap: var(--a2ui-spacing-m);
padding: var(--a2ui-spacing-m) calc(var(--a2ui-spacing-m) * 1.75);
margin: calc(var(--a2ui-spacing-xs) * 2) 0;
border-radius: var(--a2ui-border-radius);
background-color: var(--a2ui-color-primary);
color: var(--a2ui-color-on-primary);
border: var(--a2ui-border-width) solid var(--a2ui-color-border);
border-radius: var(--a2ui-button-border-radius, var(--a2ui-border-radius));
background: var(--a2ui-color-surface);
box-shadow: none;
color: var(--a2ui-color-on-secondary);
font-weight: normal;
transition:
background-color 0.2s ease,
transform 0.12s ease,
opacity 0.12s ease,
background-color 0.12s ease;
opacity 0.12s ease;
}

.button.ui-active {
background-color: var(--a2ui-color-primary-hover);
background-color: var(--a2ui-color-secondary-hover);
}

.button-primary {
background-color: var(--a2ui-color-primary);
border: none;
color: var(--a2ui-color-on-primary);
}

.button-borderless {
background-color: transparent;
background: none;
border: none;
padding-left: 0;
padding-right: 0;
color: var(--a2ui-color-primary);
}

.button-disabled {
opacity: 0.55;
}

.button-disabled.ui-active {
background-color: inherit;
}

.button .text-body,
.button .text-caption,
.button .text-h1,
Expand Down
1 change: 0 additions & 1 deletion packages/genui/a2ui/styles/catalog/Image.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@

.a2ui-image {
flex-shrink: 0;
padding: 8px;
overflow: hidden;
}

Expand Down
11 changes: 11 additions & 0 deletions packages/genui/a2ui/styles/theme.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading