Is it possible to have dynamic prop keys/visible names?

I’m trying to create a code component that is using the external API with context-dependent methods and named args.

Speaking specifically, it is calling Ethereum blockchain, but this fact can be omitted within this question and considered as just some generic RPC API like call(entityID: string, methodName: string, ...args: string[]), where possible methodName options is list based on entityID (like getMethods(entityID)), and args are named tuple based on entityID and methodName (like getArgs(entityID, methodName)).

E.g. for specific kind of entity, args for method balanceOf are expected as named tuple [address: string], and for allowance it will be [owner: string, spender: string].

I’m currently using arg1, arg2...arg6 as tuple entities, but I want to make better developer experience for users of this component.

I’m currently using a hack to render the dynamic description, but it can be unstable in future. This is how things looks like when using this hack:

I wish to have some dynamic values instead of Arg1, Arg2 and so on, and I do not want to rely on hack in render of helpText and other attributes.
I’m kindly requesting for API support of at least one following options:

  • make PropType.helpText dynamic - similar to how hidden works - as type string | ContextDependentConfig<P, string>.
    (I assume it will only require to change this line to support helpText functions and change some types)
  • Same request additionally goes for PropType.required, PropType.exprHint. I personally feel less urge in this properties but I assume this will be quite useful for others.
  • expose name/displayName property as string | ContextDependentConfig<P, string> to make rendered key names controlled by component developer.
  • alternatively, it will be great if some attribute like dynamicProps will be implemented, that will accept ContextDependentConfig<P> as input (with props only, without current dynamic props to avoid looping) and return object similar to props, that will be rendered below main props in editor sidebar.

I understand that I can create custom prop to solve this issue, but this will be looking not so good, and I also will lose the ability to use dynamic props, which is quite important for my case.

Additionally, it will be great if refActions will be also dynamic in same manner (taking props into account), and if CodeComponentMeta.displayName will be definable in dynamic manner for better user experience, but I understand this may be way more complicated to implement.

Hello @merkle_bonsai
I think what you are looking for should be achievable via Custom Prop Controls or Editor Actions

You mentioned that you do not want to use custom prop. Is there any specific reason for this preference?

Hi @sarah_ahmed. I understand that I specifically can overwrite helpText with custom control, but this requires me to implement whole custom control instead of defining single function, which bloats the code, which will be unreasonably complicated; and adding the dynamic helpText is quite easy to implement, since I see it from the code. Actually, I can bring the PR myself if this is OK.

However, I’m mostly requesting the possibility to define what is displayed in my screenshot as “Arg1”, “Arg2” etc. Those are parts of configurator sidebar not controlled by custom control, as custom control is only rendered in right part of each line, and name of property is non-configurable and taken directly from prop key.

Only way to do it more or less visually comfortable for users is to put whole configuration (with address, method, args) to single object-typed property and custom component. But it will break the possibility to have dynamic parameters connected to args, since for custom component and single property dynamic parameters can only be connected to whole confugration object, not specific keys of confugration object.

Have you considered creating a separate code component for each method? I think that may lead to a better UX.

Another idea: If the API has some sort of schema you can work with, perhaps you could write some code to programmatically generate code component for each method.

Have you considered moving all of the props inside a custom action. Here’s a code snippet of how you can do that:

import { Button, Form, Input, Label, Radio, RadioGroup, Switch, TextField } from "react-aria-components";
import { useState } from "react";

interface PropType {
    entityID: string; methodName: string; args: any
}

const sampleData: PropType[] = [
    {
        entityID: "0x123456789ABCDEF",
        methodName: "balanceOf",
        args: { address: "string" }
    },
    {
        entityID: "0x123456789ABCDEF",
        methodName: "sayHello",
        args: { message: "string", showIcon: "boolean" }
    },
    {
        entityID: "0xABCDEF123456789",
        methodName: "allowance",
        args: { owner: "string", spender: "string" }
    },
    {
        entityID: "0xABCDEF123456789",
        methodName: "sayBye",
        args: { message: "string", labels: "array" }
    },
    {
        entityID: "0xDEF123456789ABC",
        methodName: "transfer",
        args: { recipient: "string", amount: "number" }
    },
    {
        entityID: "0x456789ABCDEF123",
        methodName: "approve",
        args: { owner: "string", spender: "string", amount: "number" }
    }
];

const getMethods = (entityId: string) => {
    return sampleData
        .filter((item: PropType) => item.entityID === entityId)
        .map((item: PropType) => item.methodName);
}

const getArgs = (entityId: string, methodName: string) => {
    const entry = sampleData.find(item => item.entityID === entityId && item.methodName === methodName);
    return entry ? entry.args : null;
}

function ABC(props: PropType) {
    console.log({ props })
    return <p>Hello World!</p>
}

function ArgProp({ studioOps, componentProps }: any) {
    const [data, setData] = useState<any>({})
    const [entityID, setEntityID] = useState("");
    const [methodName, setMethodName] = useState("");

    const args = getArgs(entityID, methodName);

    const methods = getMethods(entityID);
    return (
        <Form onSubmit={(e) => {
            e.preventDefault();
            console.log(e)
            studioOps.updateProps({ entityID, methodName, args: data })
        }}>
            <TextField name="entityId" onChange={setEntityID}>
                <Label>Entity ID</Label>
                <Input />

            </TextField>
            {methods?.length && (
                <RadioGroup name={methodName} onChange={setMethodName}>
                    <Label>Method</Label>
                    {methods.map(method => (
                        <Radio key={method} value={method}>{method}</Radio>
                    ))}
                </RadioGroup>
            )}
            {methodName &&
                Object.keys(args).map(key => (
                    args[key] === "string" ? (
                        <TextField key={key} name={key} onChange={(text) => setData((prev: any) => ({ ...prev, [key]: text }))}>
                            <Label>{key}</Label>
                            <Input />
                        </TextField>
                    ) : args[key] === "number" ? (
                        <TextField key={key} name={key} type="number">
                            <Label>{key}</Label>
                            <Input />
                        </TextField>
                    ) : args[key] === "boolean" ? (
                        <Switch name={key}>
                            {({ isSelected }) => (
                                <>
                                    {isSelected && "✅"}
                                    {key}
                                </>
                            )}
                        </Switch>
                    ) : <p>NOthing</p>
                ))
            }
            <Button type="submit">Submit!</Button>
        </Form>
    )
}

export function registerABC(loader: any) {
    loader.registerComponent(ABC, {
        name: "abc",
        displayName: "ABC ETH",
        actions: [
            {
                type: 'custom-action',
                control: ArgProp
            }
        ],
        props: {}
    });
}

I’m afraid it is not an option for me, since I do not know in advance what contracts (entities) and methods I’ll be using. I already thought about it.
I’m working on component that can work with most of published contracts on multiple networks, and it is basically impossible to predefine components for each contract used

as I wrote above, this is not an option for me. First of all, what you are using as sampleData is downloaded from server dynamically in my case, as JSON configuration, and passed further via setControlContextData.

Secondly, this will break the possibility to granularly define props (at least I’m unaware how to fix it), so e.g. user will not be able to link field with other input or global state variable, which is must-have feature for me. I’m actually even unsure if custom action state in general can be dynamically linked with specific props, as I’m mostly working with custom fields, not custom actions

We currently do not support dynamic keys/props as most use cases (including yours) seem to be achievable with custom actions. You should be able to use async functions to fetch contract data within your custom action. To link fields with other inputs, you can expose state or ref actions.