Dash-generate-components

Hi!

I am currently working on recreate the react component “WithWalletConnector” into a dash component ( concordium-dapp-libraries/packages/react-components/src/WithWalletConnector.ts at main · Concordium/concordium-dapp-libraries · GitHub )

But to start out I have made a simple example with typescript just for testing.

MyComponent.ts

import React, { Component } from 'react';
import {DashComponentProps} from './props';

type Props = {
    // Insert props
} & DashComponentProps;


export default class WalletConnectionManager extends Component<Props, any> {
    constructor(props: Props) {
        super(props);
        //this.props = props
        console.log('====================================');
        console.log(props);
        console.log('====================================');
    }
    static defaultProps: {}; //Partial<Props>

    render(): React.ReactNode {
        return (
            'SomeString'
        )
    }
}

WalletConnectionManager.defaultProps = {};

props.ts

 */
export type DashComponentProps = {
    /**
     * Unique ID to identify this component in Dash callbacks.
     */
    id?: string;
    /**
     * Unique ID to identify this component in Dash callbacks.
     */
    test?: string;
    /**
     * Update props to trigger callbacks.
     */
    setProps: (props: Record<string, any>) => void;
}

Typescript Cookiecutter
Running this with the Typescript cookiecutter works fine, but when I convert component to the actual WithWalletConnector code, I get following error when building:

error message:

> dash-generate-components ./src/ts/components tswalletwrapper -p package-info.json --r-prefix '' --jl-prefix '' --ignore \.test\.


ERROR: "__@hasInstance@9" matches reserved word pattern: /^_.*$/

Description for Tswalletwrapper.prototype is missing!

Description for Tswalletwrapper.length is missing!

Description for Tswalletwrapper.arguments is missing!

Description for Tswalletwrapper.caller is missing!

ERROR: "__@hasInstance@9" matches reserved word pattern: /^_.*$/

Description for Tswalletwrapper is missing!
extract-meta failed

I have not been able to find any information around this error. (And as a comment the typescript cookiecutter would be nice if it included a “run demo” option as in the original.)

My component (leading to above error) looks as follows:

import React, { Component } from 'react';
import {DashComponentProps} from '../props';

import { Network, WalletConnection, WalletConnectionDelegate, WalletConnector } from '@concordium/react-components';
import { errorString } from './error';



/**
 * Activation/deactivation controller of a given connector type.
 */
export interface ConnectorType {
    /**
     * Called when the connection type is being activated.
     * The connector instance returned by this method becomes the new {@link State.activeConnector activeConnector}.
     * @param component The component in which the instance is being activated.
     *                  This object doubles as the delegate to pass to new connector instances.
     * @param network The network to pass to new connector instances.
     */
    activate(component: WalletConnectionManager, network: Network): Promise<WalletConnector>;

    /**
     * Called from {@link WithWalletConnector} when the connection type is being deactivated,
     * i.e. right after {@link State.activeConnector activeConnector} has been unset from this value.
     * @param component The component in which the instance is being deactivated.
     * @param connector The connector to deactivate.
     */
    deactivate(component: WalletConnectionManager, connector: WalletConnector): Promise<void>;
}

/**
 * Produce a {@link ConnectorType} that creates a new connector instance on activation
 * and disconnects the existing one on deactivation.
 * This is the simplest connection type and should be used unless there's a reason not to.
 * @param create Factory function for creating new connector instances.
 */
export function ephemeralConnectorType(create: (c: WalletConnectionManager, n: Network) => Promise<WalletConnector>) {
    return {
        activate: create,
        deactivate: (w: WalletConnectionManager, c: WalletConnector) => c.disconnect(),
    };
}

/**
 * Produce a {@link ConnectorType} that reuse connectors between activation cycles.
 * That is, once a connector is created, it's never automatically disconnected.
 * Note that only the connector is permanent. Individual connections may still be disconnected by the application.
 * @param create Factory function for creating new connector instances.
 */
export function persistentConnectorType(create: (c: WalletConnectionManager, n: Network) => Promise<WalletConnector>) {
    const connectorPromises = new Map<WalletConnectionManager, Map<Network, Promise<WalletConnector>>>();
    return {
        activate: (component: WalletConnectionManager, network: Network) => {
            const delegateConnectorPromises =
                connectorPromises.get(component) || new Map<Network, Promise<WalletConnector>>();
            connectorPromises.set(component, delegateConnectorPromises);
            const connectorPromise = delegateConnectorPromises.get(network) || create(component, network);
            delegateConnectorPromises.set(network, connectorPromise);
            return connectorPromise;
        },
        deactivate: async () => undefined,
    };
}

/**
 * The internal state of the component.
 */
interface State {
    /**
     * The active connector type. This value is updated using {@link WalletConnectionProps.setActiveConnectorType}.
     * Changes to this value trigger activation of a connector managed by the connector type.
     * This will cause {@link activeConnector} or {@link activeConnectorError} to change depending on the outcome.
     */
    activeConnectorType: ConnectorType | undefined;

    /**
     * The active connector. Connector instances get (de)activated appropriately when {@link activeConnectorType} changes.
     *
     * It's up to the {@link ConnectorType} in {@link activeConnectorType} to implement any synchronization between
     * the active connector and {@link activeConnector}:
     * In general, it is perfectly possible for the active connection to not originate from the active connector.
     *
     * If the application disconnects the active connector manually, they must also call
     * {@link WalletConnectionProps.setActiveConnectorType} to
     */
    activeConnector: WalletConnector | undefined;

    /**
     * Any of the following kinds of errors:
     * - Error activating a connector with {@link activeConnectorType}.
     *   In this case {@link activeConnector} is undefined.
     * - Error deactivating the previous connector.
     *   In this case {@link activeConnectorType} and {@link activeConnector} are undefined.
     */
    activeConnectorError: string;

    /**
     * A map from open connections to their selected accounts or the empty string
     * if the connection doesn't have an associated account.
     */
    connectedAccounts: Map<WalletConnection, string>;

    /**
     * A map from open connections to the hash of the genesis block for the chain that the selected accounts
     * of the connections live on.
     * Connections without a selected account (or the account's chain is unknown) will not have an entry in this map.
     *
     * TODO The reported hash values are not too reliable as they're updated only when the `onChainChanged` event fires.
     *      And this doesn't happen when the connection is initiated.
     *      For WalletConnect we could do that manually as we control what chain we connect to.
     *      For BrowserWallet we don't have that option (see also https://concordium.atlassian.net/browse/CBW-633).
     */
    genesisHashes: Map<WalletConnection, string>;
}

function updateMapEntry<K, V>(map: Map<K, V>, key: K | undefined, value: V | undefined) {
    const res = new Map(map);
    if (key !== undefined) {
        if (value !== undefined) {
            res.set(key, value);
        } else {
            res.delete(key);
        }
    }
    return res;
}


/**
 * PROPS definition
 */
/* type Props = {
    // Insert props
} & DashComponentProps; */
interface Props extends DashComponentProps {
    /**
     * The network on which the connected accounts are expected to live on.
     *
     * Changes to this value will cause all connections managed by {@link State.activeConnector} to get disconnected.
     */
    network: Network; // reacting to change in 'componentDidUpdate'

    /**
     * Function for generating the child component based on the props derived from the state of this component.
     *
     * JSX automatically supplies the nested expression as this prop field, so callers usually don't set it explicitly.
     *
     * @param props Connection state and management functions.
     * @return Child component.
     */
    children: (props: WalletConnectionProps) => JSX.Element;
};

/**
 * The props to be passed to the child component.
 */
export interface WalletConnectionProps extends State {
    /**
     * The network provided to {@link WithWalletConnector} via its props.
     *
     * This is only passed for convenience as the value is always available to the child component anyway.
     */
    network: Network;

    /**
     * Function for setting or resetting {@link State.activeConnectorType activeConnectorType}.
     *
     * Any existing connector type value is deactivated and any new one is activated.
     *
     * @param type The new connector type or undefined to reset the value.
     */
    setActiveConnectorType: (type: ConnectorType | undefined) => void;
}
/**
 * This is the actual class exported to Dash. It is a rebuild of the original WithWalletConnector in order
 * to get access to the different variables without an extra wrapper.
 */
class WalletConnectionManager extends Component<Props, State> implements WalletConnectionDelegate {
    constructor(props: Props) {
        super(props);
        this.state = {
            activeConnectorType: undefined,
            activeConnector: undefined,
            activeConnectorError: '',
            genesisHashes: new Map(),
            connectedAccounts: new Map(),
        };
    }
    static defaultProps: {};

    /**
     * @see WalletConnectionProps.setActiveConnectorType
     */
    setActiveConnectorType = (type: ConnectorType | undefined) => {
        console.debug("WithWalletConnector: calling 'setActiveConnectorType'", { type, state: this.state });
        const { network } = this.props;
        const { activeConnectorType, activeConnector } = this.state;
        this.setState({
            activeConnectorType: type,
            activeConnector: undefined,
            activeConnectorError: '',
        });
        if (activeConnectorType && activeConnector) {
            activeConnectorType.deactivate(this, activeConnector).catch((err) =>
                this.setState((state) => {
                    // Don't set error if user switched connector type since initializing this connector.
                    // It's OK to show it if the user switched away and back...
                    if (state.activeConnectorType !== type) {
                        return state;
                    }
                    return { ...state, activeConnectorError: errorString(err) };
                })
            );
        }
        if (type) {
            type.activate(this, network)
                .then((connector: WalletConnector) => {
                    console.log('WithWalletConnector: setting active connector', { connector });
                    // Switch the connector (type) back in case the user changed it since initiating the connection.
                    this.setState({ activeConnectorType: type, activeConnector: connector, activeConnectorError: '' });
                })
                .catch((err) =>
                    this.setState((state) => {
                        if (state.activeConnectorType !== type) {
                            return state;
                        }
                        return { ...state, activeConnectorError: errorString(err) };
                    })
                );
        }
    };

    onAccountChanged = (connection: WalletConnection, address: string | undefined) => {
        console.debug("WithWalletConnector: calling 'onAccountChanged'", { connection, address, state: this.state });
        this.setState((state) => ({
            ...state,
            connectedAccounts: updateMapEntry(state.connectedAccounts, connection, address || ''),
        }));
    };

    onChainChanged = (connection: WalletConnection, genesisHash: string) => {
        console.debug("WithWalletConnector: calling 'onChainChanged'", { connection, genesisHash, state: this.state });
        this.setState((state) => ({
            ...state,
            genesisHashes: updateMapEntry(state.genesisHashes, connection, genesisHash),
        }));
    };

    onConnected = (connection: WalletConnection, address: string | undefined) => {
        console.debug("WithWalletConnector: calling 'onConnected'", { connection, state: this.state });
        this.onAccountChanged(connection, address);
    };

    onDisconnected = (connection: WalletConnection) => {
        console.debug("WithWalletConnector: calling 'onDisconnected'", { connection, state: this.state });
        this.setState((state) => ({
            ...state,
            connectedAccounts: updateMapEntry(state.connectedAccounts, connection, undefined),
        }));
    };

    render() {
        const { children, network } = this.props;
        //return children({ ...this.state, network, setActiveConnectorType: this.setActiveConnectorType });
        return (
            <div>
                {children({ ...this.state, network, setActiveConnectorType: this.setActiveConnectorType })};
            </div>
        )
    };

    componentDidUpdate(prevProps: Props) {
        if (prevProps.network !== this.props.network) {
            // Reset active connector and connection when user changes network.
            // In the future there may be a mechanism for negotiating with the wallet.
            this.setActiveConnectorType(undefined);
        }
    }

    componentWillUnmount() {
        // TODO Disconnect everything?
    }
}


WalletConnectionManager.defaultProps = {};
export default WalletConnectionManager;

Standard Cookiecutter
So as I did not get any further with this I tried with the original template:

Here running the demo example works as intended, but when I try to build it, I notice that it does not create the python module file as expected but no error messages, see output below:

Actual output:

> dash-generate-components ./src/lib/components walletconnectionmanager -p package-info.json --r-prefix '' --jl-prefix '' --ignore \.test\.
<-- HERE -->
Warning: a URL for bug reports was not provided. Empty string inserted.
Warning: a homepage URL was not provided. Empty string inserted.
Generated src/Walletconnectionmanager.jl
Generated Project.toml
Done in 1.85s.

Expected output:

> dash-generate-components ./src/lib/components my_dash_component -p package-info.json --r-prefix '' --jl-prefix '' --ignore \.test\.

Generated MyDashComponent.py
Generated myDashComponent.R
Generated mydashcomponent.jl
Warning: a URL for bug reports was not provided. Empty string inserted.
Warning: a homepage URL was not provided. Empty string inserted.
Generated src/MyDashComponent.jl
Generated Project.toml
Done in 2.07s.

In order to run typescript with the standard cookiecutter I have made following changes/configurations:

  • .babelrc - Inserted following in presets: “@babel/preset-typescript”
  • webpack.config.js - changed rule test arg from test: /\.jsx?$/, to test: /\.(js|jsx|ts|tsx)$/,
  • Installed - “@babel/preset-typescript”

Any help would be highly appreciated!

Hi @tel

Have you tried using this library to make the component?

To find a simple example, see this thread 📣 Introducing TypeScript Dash component generation

For other examples, the dash-bootstrap-components library and the dash-mantine-components library both use typescript.

Hi! Yes that is the first one I tried (the one I refer to as Typescript Cookiecutter) where I get the weird “__hasintance@9” error.

Hello @tel,

What os are you trying to create this on?

I am working on a ubuntu v 20.04 (focal)

@AnnMarieW I have attached full code for my component that errors using the typescript template

The typescript generator may have some trouble with multiples exports and detecting if they are actual components, I recommend putting only components code in a separate folder (eg: components) and only exporting actual components in those files.

3 Likes