Migrate all interactive Blazor components to vanilla JS for full SSR
- Replace server interactivity with vanilla JS (forms.js) for Popover, Calendar, TimePicker, NumberInput, and Counter components - Rewrite all Razor components to static SSR using data-* attributes for JS hooks - Simplify InputBase.cs (remove EventCallback, EditContext, SetValueAsync) - Remove AddInteractiveServerComponents/AddInteractiveServerRenderMode from Program.cs - Update demo pages: remove @rendermode, replace EditForm with native form - Add InteractivityGapTests.cs with 30 scoped E2E tests - Update FormsTests.cs selectors for new static SSR structure - Fix year picker navigation bug and date format mismatch in forms.js - All 126 tests passing
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
@page "/cards"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Cards</PageTitle>
|
||||
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
@page "/counter"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
<p role="status">Current count: <span id="counter-value">0</span></p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
<button class="btn btn-primary" id="counter-btn" onclick="document.getElementById('counter-value').textContent = ++window._count || (window._count=1)">Click me</button>
|
||||
|
||||
@@ -1,126 +1,49 @@
|
||||
@page "/forms"
|
||||
@rendermode InteractiveServer
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
|
||||
<PageTitle>Forms</PageTitle>
|
||||
|
||||
<div class="space-y-6 max-w-lg">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Forms Demo</h1>
|
||||
<p class="text-muted-foreground">All input components with DataAnnotations validation.</p>
|
||||
<p class="text-muted-foreground">All input components — fully static SSR with JS interactivity.</p>
|
||||
</div>
|
||||
|
||||
<EditForm EditContext="_editContext" OnSubmit="HandleSubmit" FormName="demo-form">
|
||||
<DataAnnotationsValidator />
|
||||
<form>
|
||||
|
||||
<div class="space-y-4">
|
||||
<FormField Label="Full Name" For="name" Error="@GetError(nameof(Model.Name))">
|
||||
<TextInput Id="name" @bind-Value="Model.Name" Placeholder="Jane Doe" data-testid="input-name" />
|
||||
<FormField Label="Full Name" For="name">
|
||||
<TextInput Id="name" Name="name" Placeholder="Jane Doe" data-testid="input-name" />
|
||||
</FormField>
|
||||
|
||||
<FormField Label="Email" For="email" Error="@GetError(nameof(Model.Email))">
|
||||
<TextInput Id="email" Type="email" @bind-Value="Model.Email" Placeholder="jane@example.com" data-testid="input-email" />
|
||||
<FormField Label="Email" For="email">
|
||||
<TextInput Id="email" Name="email" Type="email" Placeholder="jane@example.com" data-testid="input-email" />
|
||||
</FormField>
|
||||
|
||||
<FormField Label="Password" For="password" Error="@GetError(nameof(Model.Password))">
|
||||
<TextInput Id="password" Type="password" @bind-Value="Model.Password" Placeholder="••••••••" data-testid="input-password" />
|
||||
<FormField Label="Password" For="password">
|
||||
<TextInput Id="password" Name="password" Type="password" Placeholder="••••••••" data-testid="input-password" />
|
||||
</FormField>
|
||||
|
||||
<FormField Label="Age" For="age" Error="@GetError(nameof(Model.Age))">
|
||||
<NumberInput Id="age" @bind-Value="Model.Age" Placeholder="25" Min="0" Max="150" data-testid="input-age" />
|
||||
<FormField Label="Age" For="age">
|
||||
<NumberInput Id="age" Name="age" Placeholder="25" Min="0" Max="150" data-testid="input-age" />
|
||||
</FormField>
|
||||
|
||||
<FormField Label="Birth Date" For="birthdate" Error="@GetError(nameof(Model.BirthDate))">
|
||||
<DateInput Id="birthdate" @bind-Value="Model.BirthDate" data-testid="input-birthdate" />
|
||||
<FormField Label="Birth Date" For="birthdate">
|
||||
<DateInput Id="birthdate" Name="birthdate" data-testid="input-birthdate" />
|
||||
</FormField>
|
||||
|
||||
<FormField Label="Preferred Time" For="preferredtime" Error="@GetError(nameof(Model.PreferredTime))">
|
||||
<TimeInput Id="preferredtime" @bind-Value="Model.PreferredTime" data-testid="input-time" />
|
||||
<FormField Label="Preferred Time" For="preferredtime">
|
||||
<TimeInput Id="preferredtime" Name="preferredtime" data-testid="input-time" />
|
||||
</FormField>
|
||||
|
||||
<FormField Label="Appointment" For="appointment" Error="@GetError(nameof(Model.Appointment))">
|
||||
<DateTimeInput Id="appointment" @bind-Value="Model.Appointment" data-testid="input-appointment" />
|
||||
<FormField Label="Appointment" For="appointment">
|
||||
<DateTimeInput Id="appointment" Name="appointment" data-testid="input-appointment" />
|
||||
</FormField>
|
||||
|
||||
<div class="flex gap-2 pt-2">
|
||||
<Button Type="submit" data-testid="btn-submit">Submit</Button>
|
||||
<Button Variant="@ButtonVariant.Outline" OnClick="HandleReset" data-testid="btn-reset">Reset</Button>
|
||||
<Button Type="reset" Variant="@ButtonVariant.Outline" data-testid="btn-reset">Reset</Button>
|
||||
<Button Variant="@ButtonVariant.Destructive" Disabled="true" data-testid="btn-disabled">Disabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@if (_submitted)
|
||||
{
|
||||
<div data-testid="success-message"
|
||||
class="rounded-md border border-input bg-card p-4 text-sm text-card-foreground">
|
||||
<p class="font-medium">✓ Form submitted successfully</p>
|
||||
<p class="text-muted-foreground mt-1">Name: @_submittedName</p>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private FormModel Model { get; set; } = new();
|
||||
private EditContext _editContext = null!;
|
||||
private bool _submitted;
|
||||
private string _submittedName = "";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_editContext = new EditContext(Model);
|
||||
}
|
||||
|
||||
private string? GetError(string fieldName)
|
||||
{
|
||||
var field = _editContext.Field(fieldName);
|
||||
var messages = _editContext.GetValidationMessages(field);
|
||||
return messages.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void HandleSubmit()
|
||||
{
|
||||
_submitted = false;
|
||||
|
||||
if (!_editContext.Validate())
|
||||
return;
|
||||
|
||||
_submittedName = Model.Name!;
|
||||
_submitted = true;
|
||||
}
|
||||
|
||||
private void HandleReset()
|
||||
{
|
||||
Model = new();
|
||||
_submitted = false;
|
||||
_editContext = new EditContext(Model);
|
||||
}
|
||||
|
||||
public class FormModel
|
||||
{
|
||||
[Required(ErrorMessage = "Name is required.")]
|
||||
[StringLength(100, MinimumLength = 2, ErrorMessage = "Name must be 2–100 characters.")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Email is required.")]
|
||||
[EmailAddress(ErrorMessage = "Invalid email address.")]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Password is required.")]
|
||||
[StringLength(64, MinimumLength = 8, ErrorMessage = "Password must be 8–64 characters.")]
|
||||
public string? Password { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Age is required.")]
|
||||
[Range(1, 150, ErrorMessage = "Age must be between 1 and 150.")]
|
||||
public double? Age { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Birth date is required.")]
|
||||
public DateOnly? BirthDate { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Preferred time is required.")]
|
||||
public TimeOnly? PreferredTime { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Appointment is required.")]
|
||||
public DateTime? Appointment { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user