blazorstripe-paymentstelerik

Telerik Blazor Wizard Doesn't Re-Render on Previous Button Click


I have a Blazor Web App application. It has a Telerik Wizard. On the first step, I'm rendering a Stripe Address Web Element. Here's my App.razor:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="WizardDemo.styles.css" />
    <link rel="stylesheet" href="https://blazor.cdn.telerik.com/blazor/5.1.1/kendo-theme-bootstrap/all.css" />
    <HeadOutlet />

    <script src="https://blazor.cdn.telerik.com/blazor/5.1.1/telerik-blazor.min.js" defer></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.2/signalr.min.js"></script>

    <script src="https://js.stripe.com/v3/"></script>
</head>

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>

It has the <link/> and <script/> tags for Telerik Blazor and Stripe Web Elements. Here's the Home.razor (checkout) page:

@page "/"
@inject IJSRuntime JS
@rendermode InteractiveServer

@using Telerik.Blazor
@using Telerik.Blazor.Components

<PageTitle>Checkout</PageTitle>

<h3>Checkout</h3>

<div style="width: 600px; margin: 0 auto;">
    <TelerikWizard StepperPosition="@Position">
        <WizardSteps>
            <WizardStep Label="Address" Disabled="StepsDisable[0]">
                <Content>
                    <form id="address-form">
                        <h3>Address</h3>
                        <div id="address-element">
                            <!-- Elements will create form elements here -->
                        </div>
                    </form>
                </Content>
            </WizardStep>
            <WizardStep Label="Payment" Disabled="StepsDisable[1]">
                <Content>
                    <h1>Payment Entry</h1>
                </Content>
            </WizardStep>
            <WizardStep Label="Review" Disabled="StepsDisable[2]">
                <Content>
                    <h1>Confirmation</h1>
                </Content>
            </WizardStep>
        </WizardSteps>
        <WizardSettings>
            <WizardStepperSettings Linear="true" StepType="StepperStepType.Steps" />
        </WizardSettings>
    </TelerikWizard>
</div>

@code {
    IJSObjectReference? module;

    public WizardStepperPosition Position { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/Home.razor.js");
            await module.InvokeVoidAsync("initializeAddress");
        }
    }

    public Dictionary<int, bool> StepsDisable { get; set; } = new Dictionary<int, bool>
    {
        {0, false},
        {1, false},
        {2, false},
    };

    public List<Step> Steps { get; set; } = new List<Step>
    {
        new Step { Index = 0, Text="Step 1" },
        new Step { Index = 1, Text="Step 2" },
        new Step { Index = 2, Text="Step 3" }
    };

    public class Step
    {
        public int Index { get; set; }

        public string? Text { get; set; }
    }
}

It's a Server page that has the TelerikWizard. Notice that my first Step has a form with an address-form id. In OnAfterRender it imports the Home.razor.js component and then calls the initialzeAddress function, shown next:

let elements = {};

export const initializeAddress = () => {
    // Set your publishable key: remember to change this to your live publishable key in production
    // See your keys here: https://dashboard.stripe.com/apikeys
    const stripe = Stripe('<my-public-test-key>');

    const options = {
        // Fully customizable with appearance API.
        appearance: { /* ... */ }
    };

    // Only need to create this if no elements group exist yet.
    // Create a new Elements instance if needed, passing the
    // optional appearance object.
    elements = stripe.elements(/*options*/);

    // Create and mount the Address Element in shipping mode
    const addressElement = elements.create("address", {
        mode: "billing",
    });
    addressElement.mount("#address-element");
}


export const handleNextStep = async () => {
    const addressElement = elements.getElement('address');

    return await addressElement.getValue();
};

This successfully loads the Stripe Address Web element when first opening the page. I can fill in the form and it works correctly.

The problem happens when I click the Next button and then click the Previous button, bringing me back to the Address Step. I expect to see the Address. However, I see nothing - the Address form does not render at all.

How do I get the Address to render when I click the Previous button to return to that step?

<div style="width: 600px; margin: 0 auto;">
    <TelerikWizard StepperPosition="@Position">
        <WizardSteps>
            <WizardStep Label="Address" Disabled="StepsDisable[0]">
                <Content>
                    <CheckoutAddress />
                </Content>
            </WizardStep>
            <WizardStep Label="Payment" Disabled="StepsDisable[1]">
                <Content>
                    <h1>Payment Entry</h1>
                </Content>
            </WizardStep>
            <WizardStep Label="Review" Disabled="StepsDisable[2]">
                <Content>
                    <h1>Confirmation</h1>
                </Content>
            </WizardStep>
        </WizardSteps>
        <WizardSettings>
            <WizardStepperSettings Linear="true" StepType="StepperStepType.Steps" />
        </WizardSettings>
    </TelerikWizard>
</div>

That just replaces the HTML with a CheckoutAddress component, shown here:

@inject IJSRuntime JS

<form id="address-form">
    <h3>Address</h3>
    <div id="address-element">
        <!-- Elements will create form elements here -->
    </div>
</form>

@code {
    IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/CheckoutAddress.razor.js");
            await module.InvokeVoidAsync("initializeAddress");
        }
    }
}

And the JavaScript hasn't changed, except that it's now a module of the component, CheckoutAddress.razor.js.


Solution

  • When you go back, the <WizardStep Label="Address" Disabled="StepsDisable[0]"> part will be re-rendered by Blazor (with the initial form inside).

    But the rest of the page will not have a (first) render again. The await module.InvokeVoidAsync("initializeAddress"); line does not execute again.

    It probably only works because the address form is the first Step and immediately visable. Put it on step 2 and it will never show.

    If the TelerikWizard has some StepChanged event then that would be the place to initialize again.

    Otherwise, wrap the address form in a Blazor component and use the OnAfterRender code there.