Created
January 22, 2026 12:36
-
-
Save sunmeat/4cffc5e0d63ad3385ae511fe2de236cd to your computer and use it in GitHub Desktop.
обробка форм з EditForm + DataAnnotations валідація
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @using System.ComponentModel.DataAnnotations | |
| @using Microsoft.AspNetCore.Components.Forms | |
| @* ContactInfo.razor — обробка форм з EditForm + DataAnnotations валідація *@ | |
| <div class="contacts-wrapper"> | |
| <div class="contacts-container"> | |
| <div class="header-section"> | |
| <h3 class="title">Контактна інформація</h3> | |
| <div class="title-underline"></div> | |
| </div> | |
| <div class="contact-display"> | |
| <div class="contact-item email-item"> | |
| <span class="icon">✉️</span> | |
| <span class="label">Основний email:</span> | |
| <span class="value">@Email</span> | |
| </div> | |
| @if (ShowPhone) | |
| { | |
| <div class="contact-item phone-item fade-in"> | |
| <span class="icon">📞</span> | |
| <span class="label">Телефон:</span> | |
| <span class="value">@Phone</span> | |
| </div> | |
| } | |
| </div> | |
| <!-- форма редагування з валідацією --> | |
| <div class="form-section"> | |
| <EditForm Model="FormModel" OnValidSubmit="HandleValidSubmit"> | |
| <DataAnnotationsValidator /> | |
| <ValidationSummary /> | |
| <div class="input-group checkbox-group"> | |
| <label class="checkbox-label"> | |
| <InputCheckbox @bind-Value="FormModel.ShowPhone" /> | |
| <span class="checkbox-text">Показати номер телефону</span> | |
| <span class="checkbox-custom"></span> | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label class="input-label"> | |
| <span class="label-text">Альтернативний email:</span> | |
| <InputText @bind-Value="FormModel.AlternativeEmail" | |
| class="input-field" | |
| placeholder="наприклад, robota@example.com" /> | |
| <ValidationMessage For="@(() => FormModel.AlternativeEmail)" /> | |
| <small class="input-hint"> | |
| @(string.IsNullOrEmpty(FormModel.AlternativeEmail) | |
| ? "Поле не заповнено" | |
| : $"✓ Введено: {FormModel.AlternativeEmail}") | |
| </small> | |
| </label> | |
| </div> | |
| <div class="input-group"> | |
| <label class="input-label"> | |
| <span class="label-text">Коротка біо / примітка:</span> | |
| <InputTextArea @bind-Value="FormModel.Bio" | |
| rows="4" | |
| class="textarea-field" | |
| placeholder="Напишіть пару слів про себе..." /> | |
| <ValidationMessage For="@(() => FormModel.Bio)" /> | |
| <small class="char-counter">@FormModel.Bio?.Length / 500 символів</small> | |
| </label> | |
| </div> | |
| <div class="input-row-dual"> | |
| <div class="input-group half-width"> | |
| <label class="input-label"> | |
| <span class="label-text">Роки досвіду в .NET:</span> | |
| <div class="number-input-wrapper"> | |
| <InputNumber @bind-Value="FormModel.YearsOfExperience" | |
| class="input-field number-field" | |
| min="0" max="50" step="1" /> | |
| <ValidationMessage For="@(() => FormModel.YearsOfExperience)" /> | |
| <span class="number-badge">@FormModel.YearsOfExperience років</span> | |
| </div> | |
| </label> | |
| </div> | |
| <div class="input-group half-width"> | |
| <label class="input-label"> | |
| <span class="label-text">Дата народження:</span> | |
| <InputDate @bind-Value="FormModel.BirthDate" | |
| class="input-field date-field" /> | |
| <ValidationMessage For="@(() => FormModel.BirthDate)" /> | |
| </label> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label class="input-label"> | |
| <span class="label-text">Улюблена технологія:</span> | |
| <div class="select-wrapper"> | |
| <InputSelect @bind-Value="FormModel.FavoriteTech" | |
| class="input-field select-field"> | |
| <option value="">Оберіть технологію...</option> | |
| <option value="Blazor">🔥 Blazor</option> | |
| <option value="MAUI">📱 .NET MAUI</option> | |
| <option value="AspNet">🌐 ASP.NET Core</option> | |
| <option value="EfCore">💾 Entity Framework</option> | |
| </InputSelect> | |
| <span class="select-arrow">▼</span> | |
| </div> | |
| <ValidationMessage For="@(() => FormModel.FavoriteTech)" /> | |
| </label> | |
| </div> | |
| <div class="input-group radio-section"> | |
| <span class="label-text">Режим рендерингу:</span> | |
| <InputRadioGroup @bind-Value="FormModel.RenderMode" class="radio-group"> | |
| <label class="radio-label"> | |
| <InputRadio Value="@("Server")" /> | |
| <span class="radio-custom"></span> | |
| <span class="radio-text"> | |
| <strong>Server</strong> | |
| <small>Рендеринг на сервері</small> | |
| </span> | |
| </label> | |
| <label class="radio-label"> | |
| <InputRadio Value="@("WebAssembly")" /> | |
| <span class="radio-custom"></span> | |
| <span class="radio-text"> | |
| <strong>WebAssembly</strong> | |
| <small>Виконання в браузері</small> | |
| </span> | |
| </label> | |
| <label class="radio-label"> | |
| <InputRadio Value="@("Auto")" /> | |
| <span class="radio-custom"></span> | |
| <span class="radio-text"> | |
| <strong>Auto</strong> | |
| <small>Автоматичний вибір</small> | |
| </span> | |
| </label> | |
| </InputRadioGroup> | |
| <ValidationMessage For="@(() => FormModel.RenderMode)" /> | |
| <div class="radio-indicator"> | |
| <span class="indicator-label">Обрано:</span> | |
| <span class="indicator-value">@FormModel.RenderMode</span> | |
| </div> | |
| </div> | |
| <button type="submit" class="submit-button">Зберегти зміни</button> | |
| </EditForm> | |
| </div> | |
| </div> | |
| </div> | |
| @code { | |
| [Parameter] public string Email { get; set; } = "sunmeatrich@gmail.com"; | |
| [Parameter] public string Phone { get; set; } = "+380 (63) 03-000-35"; | |
| private ContactFormModel FormModel { get; set; } = new(); | |
| private bool ShowPhone => FormModel.ShowPhone; | |
| protected override void OnInitialized() | |
| { | |
| // початкові значення можна взяти з параметрів або деінде | |
| FormModel.AlternativeEmail = "oleksandr@itstep.academy"; | |
| FormModel.Bio = "Full-stack розробник на .NET / Blazor"; | |
| FormModel.YearsOfExperience = 5; | |
| FormModel.BirthDate = new DateTime(1989, 3, 10); | |
| FormModel.FavoriteTech = "Blazor"; | |
| FormModel.RenderMode = "Auto"; | |
| } | |
| private async Task HandleValidSubmit() | |
| { | |
| // тут можна зберегти дані (наприклад, виклик API, оновлення стану тощо) | |
| await Task.Delay(800); // імітація асинхронної операції | |
| // після збереження можна показати повідомлення або оновити UI | |
| StateHasChanged(); // оновлення UI після збереження | |
| } | |
| } | |
| @* окрема модель з DataAnnotations для валідації, при потребі можна винести в окремий файл під назвою ContactFormModel.cs *@ | |
| @code { | |
| public class ContactFormModel | |
| { | |
| public bool ShowPhone { get; set; } | |
| [EmailAddress(ErrorMessage = "Некоректний формат email")] | |
| [Required(ErrorMessage = "Альтернативний email обов'язковий")] | |
| public string? AlternativeEmail { get; set; } | |
| [MaxLength(500, ErrorMessage = "Біо не може перевищувати 500 символів")] | |
| public string? Bio { get; set; } | |
| [Range(0, 50, ErrorMessage = "Досвід від 0 до 50 років")] | |
| public int YearsOfExperience { get; set; } | |
| [Required(ErrorMessage = "Дата народження обов'язкова")] | |
| public DateTime? BirthDate { get; set; } | |
| [Required(ErrorMessage = "Оберіть улюблену технологію")] | |
| public string? FavoriteTech { get; set; } | |
| [Required(ErrorMessage = "Оберіть режим рендерингу")] | |
| public string? RenderMode { get; set; } | |
| } | |
| } | |
| <style> | |
| :root { | |
| --bg: #ffffff; | |
| --surface: #f9fafb; | |
| --border: #d1d5db; | |
| --border-soft: #e5e7eb; | |
| --text: #111827; | |
| --muted: #6b7280; | |
| --primary: #2563eb; | |
| --primary-focus: rgba(37,99,235,.25); | |
| --danger: #dc2626; | |
| --radius: 12px; | |
| } | |
| .contacts-wrapper { | |
| max-width: 680px; | |
| margin: 2.5rem auto; | |
| padding: 2.2rem 2.4rem; | |
| background: var(--bg); | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border-soft); | |
| font-family: system-ui, -apple-system, Segoe UI, sans-serif; | |
| color: var(--text); | |
| } | |
| .header-section { | |
| margin-bottom: 2rem; | |
| text-align: center; | |
| } | |
| .title { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| margin: 0; | |
| letter-spacing: .2px; | |
| } | |
| .title-underline { | |
| width: 44px; | |
| height: 3px; | |
| background: var(--primary); | |
| border-radius: 3px; | |
| margin: .6rem auto 0; | |
| } | |
| .contact-display { | |
| background: var(--surface); | |
| border: 1px solid var(--border-soft); | |
| border-radius: var(--radius); | |
| padding: 1.2rem 1.4rem; | |
| margin-bottom: 2rem; | |
| } | |
| .contact-item { | |
| display: flex; | |
| align-items: center; | |
| gap: .75rem; | |
| font-size: .95rem; | |
| } | |
| .contact-item + .contact-item { | |
| margin-top: .6rem; | |
| } | |
| .icon { | |
| font-size: 1.25rem; | |
| } | |
| .label { | |
| color: var(--muted); | |
| min-width: 135px; | |
| } | |
| .value { | |
| font-weight: 500; | |
| } | |
| .form-section { | |
| display: grid; | |
| gap: 1.6rem; | |
| } | |
| .input-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: .4rem; | |
| } | |
| .label-text { | |
| font-size: .9rem; | |
| font-weight: 500; | |
| } | |
| input, | |
| textarea, | |
| select { | |
| width: 100%; | |
| padding: .7rem .8rem; | |
| font-size: .95rem; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| background: #fff; | |
| transition: border-color .15s, box-shadow .15s; | |
| } | |
| input:focus, | |
| textarea:focus, | |
| select:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px var(--primary-focus); | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 95px; | |
| } | |
| .input-hint, | |
| .char-counter { | |
| font-size: .8rem; | |
| color: var(--muted); | |
| } | |
| .input-row-dual { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 1.2rem; | |
| } | |
| .number-input-wrapper { | |
| position: relative; | |
| } | |
| .number-badge { | |
| position: absolute; | |
| right: .6rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| font-size: .75rem; | |
| color: var(--muted); | |
| background: var(--surface); | |
| padding: .2rem .45rem; | |
| border-radius: 999px; | |
| pointer-events: none; | |
| } | |
| .checkbox-group input[type="checkbox"] { | |
| width: 16px; | |
| height: 16px; | |
| accent-color: var(--primary); | |
| cursor: pointer; | |
| } | |
| .checkbox-label { | |
| display: flex; | |
| align-items: center; | |
| gap: .6rem; | |
| font-size: .9rem; | |
| cursor: pointer; | |
| } | |
| .checkbox-custom { | |
| display: none; | |
| } | |
| .radio-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: .6rem; | |
| margin-top: .4rem; | |
| } | |
| .radio-label { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: .55rem; | |
| cursor: pointer; | |
| font-size: .9rem; | |
| } | |
| .radio-label input[type="radio"] { | |
| margin-top: 2px; | |
| width: 16px; | |
| height: 16px; | |
| accent-color: var(--primary); | |
| cursor: pointer; | |
| } | |
| .radio-custom { | |
| display: none; | |
| } | |
| .radio-text strong { | |
| display: block; | |
| font-weight: 500; | |
| } | |
| .radio-text small { | |
| display: block; | |
| font-size: .78rem; | |
| color: var(--muted); | |
| } | |
| .radio-indicator { | |
| margin-top: .4rem; | |
| font-size: .8rem; | |
| color: var(--muted); | |
| } | |
| .validation-message { | |
| font-size: .8rem; | |
| color: var(--danger); | |
| } | |
| .invalid { | |
| border-color: var(--danger); | |
| background: #fef2f2; | |
| } | |
| .submit-button { | |
| margin-top: 1.4rem; | |
| padding: .75rem 1.6rem; | |
| border-radius: 10px; | |
| border: none; | |
| background: var(--primary); | |
| color: #fff; | |
| font-size: .95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| } | |
| .submit-button:hover { | |
| filter: brightness(.95); | |
| } | |
| @@media (max-width: 560px) { | |
| .input-row-dual { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment