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
.
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.