зеркало из https://github.com/telerik/blazor-docs.git
754 строки
26 KiB
Markdown
754 строки
26 KiB
Markdown
|
---
|
||
|
title: In-Place Editor Component
|
||
|
description: Learn how to create a custom inplace editor component, which blends seamlessly in other web page text content.
|
||
|
type: how-to
|
||
|
page_title: How to Implement In-Place Editor Component
|
||
|
slug: kb-in-place-editor
|
||
|
position:
|
||
|
tags: telerik, blazor, inplace, in place
|
||
|
ticketid:
|
||
|
res_type: kb
|
||
|
---
|
||
|
|
||
|
## Environment
|
||
|
|
||
|
<table>
|
||
|
<tbody>
|
||
|
<tr>
|
||
|
<td>Product</td>
|
||
|
<td>UI for Blazor</td>
|
||
|
</tr>
|
||
|
</tbody>
|
||
|
</table>
|
||
|
|
||
|
## Description
|
||
|
|
||
|
This KB article demonstrates and describes how to create a custom `InPlaceEditor` component. The article also answers the following questions:
|
||
|
|
||
|
* How to create an in-place editor, which looks like text when in read mode and switches to an input component when editable?
|
||
|
* How to toggle between text content and an editor to allow users to edit something in place?
|
||
|
|
||
|
## Solution
|
||
|
|
||
|
The sample below uses an algorithm which toggles between read-only UI and an editable component on user click and blur.
|
||
|
|
||
|
### How It Works
|
||
|
|
||
|
* `InPlaceEditor` is a generic component. It supports strings and most value types, including nullable types.
|
||
|
* Initially, the component renders a clickable [Button]({%slug components/button/overview%}) with [`Clear` `FillMode`]({%slug button-appearance%}) that shows the current `Value`.
|
||
|
* The component detects the type of its `Value` and renders the appropriate Telerik editor:
|
||
|
* [CheckBox]({%slug checkbox-overview%}) for `bool`
|
||
|
* [DatePicker]({%slug components/datepicker/overview%}) for `DateTime` and `DateOnly`
|
||
|
* [NumericTextBox]({%slug components/numerictextbox/overview%}) for `int`, `double`, `decimal`, and the other numeric types
|
||
|
* [TextBox]({%slug components/textbox/overview%}) for `string`
|
||
|
* [TimePicker]({%slug components/timepicker/overview%}) for `TimeOnly`
|
||
|
* If the `Width` parameter is not set, the In-Place Editor approximately matches the width of its editor components to the current `Value` length. The component uses a `monospace` `font-family` to make this easier.
|
||
|
* The component features a `ReadOnly` mode that controls the editability, for example, depending on user permissions.
|
||
|
* The `DisplayFormat` parameter affects the `Value` consistently in both read mode and edit mode.
|
||
|
* The `Placeholder` parameter provides a helper label that will show when the `Value` is `null` or empty.
|
||
|
* The `ShowIcons` parameter controls the visibility of optional [SVG Icons]({%slug common-features-icons%}}#svgicon-component). The icons hint users about the ability to edit the component `Value` or provide clickable **Save** and **Cancel** commands in edit mode. The parameter is of type `InPlaceEditorShowIcons`, which is a custom enum and must be imported in both `InPlaceEditor.razor` and all `.razor` files that use `InPlaceEditor`.
|
||
|
* The `Class` parameter allows you to apply custom styles.
|
||
|
* The `Title` parameter allows you to show a tooltip hint on read mode.
|
||
|
* To [see invalid state styling and validation messages in Forms]({%slug inputs-kb-validate-child-component%}), pass the respective `ValueExpression` values to the `InPlaceEditor` component.
|
||
|
* `InPlaceEditor.razor.css` is a <a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation" target="_blank">CSS isolation file</a>. It depends on a `YourAppName.styles.css` file in `App.razor` to load.
|
||
|
|
||
|
### Example
|
||
|
|
||
|
The features and business logic below can be subject to additional customizations and enhancements.
|
||
|
|
||
|
To run the code successfully:
|
||
|
|
||
|
* Replace `YourAppName` with the actual root namespace of your app.
|
||
|
* Make sure your app supports CSS isolation and loads a `YourAppName.styles.css` file. Browser caching of this file can prevent the InPlaceEditor styles from showing.
|
||
|
|
||
|
<div class="skip-repl"></div>
|
||
|
|
||
|
````Home.razor
|
||
|
@* import InPlaceEditorType enum *@
|
||
|
@using YourAppName.Models
|
||
|
|
||
|
@using System.ComponentModel.DataAnnotations
|
||
|
|
||
|
<h1>InPlaceEditor Component</h1>
|
||
|
|
||
|
<p>
|
||
|
This in-place editor component works with strings and value types, including nullables, for example:
|
||
|
|
||
|
<InPlaceEditor @bind-Value="@NumericValue"
|
||
|
DisplayFormat="C2"
|
||
|
Placeholder="Enter Number..." />
|
||
|
|
||
|
The component supports custom styles and responsive textbox width that depends on the value:
|
||
|
|
||
|
<InPlaceEditor @bind-Value="@StringValue"
|
||
|
Class="primary-color"
|
||
|
ShowIcons="@InPlaceEditorShowIcons.Hover" />
|
||
|
|
||
|
The icon can be visible only on hover:
|
||
|
|
||
|
<InPlaceEditor @bind-Value="@DateValue"
|
||
|
Class="primary-color"
|
||
|
DisplayFormat="d"
|
||
|
ShowIcons="@InPlaceEditorShowIcons.Hover" />
|
||
|
|
||
|
(unless the value is empty) or never:
|
||
|
|
||
|
<InPlaceEditor @bind-Value="@TimeValue"
|
||
|
Class="primary-color"
|
||
|
DisplayFormat="HH:mm"
|
||
|
ShowIcons="@InPlaceEditorShowIcons.Never" />
|
||
|
|
||
|
You can even edit booleans:
|
||
|
|
||
|
<InPlaceEditor @bind-Value="@BoolValue"
|
||
|
Class="primary-color" />
|
||
|
</p>
|
||
|
|
||
|
<h2>Configuration</h2>
|
||
|
|
||
|
<ul>
|
||
|
<li>
|
||
|
<label for="editor-placeholder">Placeholder: </label>
|
||
|
<TelerikTextBox @bind-Value="@InPlaceEditorPlaceholder"
|
||
|
Id="editor-placeholder"
|
||
|
ShowClearButton="true"
|
||
|
Width="180px" />
|
||
|
</li>
|
||
|
<li><label><TelerikCheckBox @bind-Value="@InPlaceEditorReadOnly" /> Read Only</label></li>
|
||
|
<li>
|
||
|
<span>Show Icon: </span>
|
||
|
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Single">
|
||
|
<ButtonGroupToggleButton Selected="@( InPlaceEditorShowIcons == InPlaceEditorShowIcons.Always )"
|
||
|
OnClick="@( () => InPlaceEditorShowIcons = InPlaceEditorShowIcons.Always )">
|
||
|
Always
|
||
|
</ButtonGroupToggleButton>
|
||
|
<ButtonGroupToggleButton Selected="@( InPlaceEditorShowIcons == InPlaceEditorShowIcons.Hover )"
|
||
|
OnClick="@( () => InPlaceEditorShowIcons = InPlaceEditorShowIcons.Hover )">
|
||
|
Hover
|
||
|
</ButtonGroupToggleButton>
|
||
|
<ButtonGroupToggleButton Selected="@( InPlaceEditorShowIcons == InPlaceEditorShowIcons.Never )"
|
||
|
OnClick="@( () => InPlaceEditorShowIcons = InPlaceEditorShowIcons.Never )">
|
||
|
Never
|
||
|
</ButtonGroupToggleButton>
|
||
|
</TelerikButtonGroup>
|
||
|
</li>
|
||
|
<li>
|
||
|
<label for="editor-title">Title: </label>
|
||
|
<TelerikTextBox @bind-Value="@InPlaceEditorTitle"
|
||
|
Id="editor-title"
|
||
|
ShowClearButton="true"
|
||
|
Width="180px" />
|
||
|
</li>
|
||
|
<li>
|
||
|
<label for="editor-width">Editor Width: </label>
|
||
|
<TelerikNumericTextBox @bind-Value="@InPlaceEditorWidth"
|
||
|
Format="# px"
|
||
|
Id="editor-width"
|
||
|
Width="120px" />
|
||
|
</li>
|
||
|
</ul>
|
||
|
|
||
|
<p>
|
||
|
In Place Editor:
|
||
|
<InPlaceEditor @bind-Value="@InPlaceEditorValue"
|
||
|
Class="primary-color"
|
||
|
Placeholder="@InPlaceEditorPlaceholder"
|
||
|
ReadOnly="@InPlaceEditorReadOnly"
|
||
|
ShowIcons="@InPlaceEditorShowIcons"
|
||
|
Title="@InPlaceEditorTitle"
|
||
|
Width="@( InPlaceEditorWidth.HasValue ? $"{InPlaceEditorWidth}px" : null )" />
|
||
|
</p>
|
||
|
|
||
|
<h2>Form Validation</h2>
|
||
|
|
||
|
<TelerikForm Model="@Employee">
|
||
|
<FormValidation>
|
||
|
<DataAnnotationsValidator />
|
||
|
</FormValidation>
|
||
|
<FormItems>
|
||
|
<FormItem Field="@nameof(Person.Name)">
|
||
|
<Template>
|
||
|
Name:
|
||
|
<InPlaceEditor Value="@Employee.Name"
|
||
|
ValueChanged="@( (string newValue) => Employee.Name = newValue )"
|
||
|
ValueExpression="@( () => Employee.Name )"
|
||
|
Placeholder="Enter Name..." />
|
||
|
<TelerikValidationMessage For="@( () => Employee.Name )" />
|
||
|
</Template>
|
||
|
</FormItem>
|
||
|
<FormItem Field="@nameof(Person.BirthDate)">
|
||
|
<Template>
|
||
|
Hire Date:
|
||
|
<InPlaceEditor Value="@Employee.BirthDate"
|
||
|
ValueChanged="@( (DateTime? newValue) => Employee.BirthDate = newValue )"
|
||
|
ValueExpression="@( () => Employee.BirthDate )"
|
||
|
DisplayFormat="d"
|
||
|
Placeholder="Enter Date..."
|
||
|
T="@(DateTime?)" />
|
||
|
<TelerikValidationMessage For="@( () => Employee.BirthDate )" />
|
||
|
</Template>
|
||
|
</FormItem>
|
||
|
</FormItems>
|
||
|
</TelerikForm>
|
||
|
|
||
|
<style>
|
||
|
h1 {
|
||
|
font-size: 1.5rem;
|
||
|
}
|
||
|
|
||
|
h2 {
|
||
|
font-size: 1.2rem;
|
||
|
}
|
||
|
|
||
|
.primary-color {
|
||
|
color: var(--kendo-color-primary);
|
||
|
}
|
||
|
</style>
|
||
|
|
||
|
@code {
|
||
|
private bool BoolValue { get; set; }
|
||
|
private DateTime? DateValue { get; set; } = DateTime.Now;
|
||
|
private decimal? NumericValue { get; set; } = 1.23m;
|
||
|
private string StringValue { get; set; } = "foo bar";
|
||
|
private TimeOnly TimeValue { get; set; } = TimeOnly.FromDateTime(DateTime.Now);
|
||
|
|
||
|
private string InPlaceEditorPlaceholder { get; set; } = "Enter Value...";
|
||
|
private bool InPlaceEditorReadOnly { get; set; }
|
||
|
private InPlaceEditorShowIcons InPlaceEditorShowIcons { get; set; } = InPlaceEditorShowIcons.Always;
|
||
|
private string InPlaceEditorTitle { get; set; } = "Edit Sample Value";
|
||
|
private string InPlaceEditorValue { get; set; } = "foo bar";
|
||
|
private int? InPlaceEditorWidth { get; set; } = 120;
|
||
|
|
||
|
private Person Employee { get; set; } = new();
|
||
|
|
||
|
public class Person
|
||
|
{
|
||
|
[Required]
|
||
|
public string? Name { get; set; } = string.Empty;
|
||
|
[Required]
|
||
|
public DateTime? BirthDate { get; set; }
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
````InPlaceEditor.razor
|
||
|
@* import InPlaceEditorType enum *@
|
||
|
@using YourAppName.Models
|
||
|
|
||
|
@using System.Globalization
|
||
|
@using System.Linq.Expressions
|
||
|
|
||
|
@typeparam T
|
||
|
|
||
|
<span class="@ClassToRender"
|
||
|
@onkeydown="@OnSpanKeyDown"
|
||
|
@onfocusin="@OnSpanFocusIn">
|
||
|
@if (IsInEditMode)
|
||
|
{
|
||
|
switch (ValueEditorType)
|
||
|
{
|
||
|
case InPlaceEditorType.CheckBox:
|
||
|
<TelerikCheckBox @ref="@CheckBoxRef"
|
||
|
Value="@Convert.ToBoolean(Value)"
|
||
|
ValueChanged="@( (bool newValue) => OnEditorValueChanged(newValue) )"
|
||
|
ValueExpression="@( ValueExpression as Expression<Func<bool>> )"
|
||
|
OnBlur="@OnEditorChange"
|
||
|
Class="@CheckBoxClass" />
|
||
|
break;
|
||
|
case InPlaceEditorType.DatePicker:
|
||
|
<TelerikDatePicker @ref="@DatePickerRef"
|
||
|
Value="@Value"
|
||
|
ValueChanged="@( (T newValue) => OnEditorValueChanged(newValue!) )"
|
||
|
ValueExpression="@( ValueExpression as Expression<Func<T>> )"
|
||
|
Format="@DisplayFormat"
|
||
|
T="@T"
|
||
|
OnChange="@OnEditorChange"
|
||
|
Class="@InputClass"
|
||
|
Width="@GetEditorWidth(InPlaceEditorType.DatePicker)" />
|
||
|
break;
|
||
|
case InPlaceEditorType.NumericTextBox:
|
||
|
<TelerikNumericTextBox @ref="@NumericTextBoxRef"
|
||
|
Value="@Value"
|
||
|
ValueChanged="@( (T newValue) => OnEditorValueChanged(newValue!) )"
|
||
|
ValueExpression="@( ValueExpression as Expression<Func<T>> )"
|
||
|
Format="@DisplayFormat"
|
||
|
OnChange="@OnEditorChange"
|
||
|
T="@T"
|
||
|
Class="@InputClass"
|
||
|
Width="@GetEditorWidth(InPlaceEditorType.NumericTextBox)" />
|
||
|
break;
|
||
|
case InPlaceEditorType.TimePicker:
|
||
|
<TelerikTimePicker @ref="@TimePickerRef"
|
||
|
Value="@Value"
|
||
|
ValueChanged="@( (T newValue) => OnEditorValueChanged(newValue!) )"
|
||
|
ValueExpression="@( ValueExpression as Expression<Func<T>> )"
|
||
|
Class="@InputClass"
|
||
|
Format="@DisplayFormat"
|
||
|
OnChange="@OnEditorChange"
|
||
|
T="@T"
|
||
|
Width="@GetEditorWidth(InPlaceEditorType.TimePicker)" />
|
||
|
break;
|
||
|
default:
|
||
|
<TelerikTextBox @ref="@TextBoxRef"
|
||
|
Value="@Value?.ToString()"
|
||
|
ValueChanged="@( (string newValue) => OnEditorValueChanged(newValue) )"
|
||
|
ValueExpression="@( ValueExpression as Expression<Func<string>> )"
|
||
|
Class="@InputClass"
|
||
|
OnChange="@OnEditorChange"
|
||
|
Width="@GetEditorWidth(InPlaceEditorType.TextBox)" />
|
||
|
break;
|
||
|
}
|
||
|
if (ShouldRenderEditIcon)
|
||
|
{
|
||
|
<TelerikButton Class="@ButtonClass"
|
||
|
Icon="@SvgIcon.Save"
|
||
|
FillMode="@ThemeConstants.Button.FillMode.Clear"
|
||
|
OnClick="@OnSaveButtonClick" />
|
||
|
<TelerikButton Class="@ButtonClass"
|
||
|
Icon="@SvgIcon.Cancel"
|
||
|
FillMode="@ThemeConstants.Button.FillMode.Clear"
|
||
|
OnClick="@OnCancelButtonClick" />
|
||
|
}
|
||
|
}
|
||
|
else if (!ReadOnly)
|
||
|
{
|
||
|
<TelerikButton @ref="@EditButtonRef"
|
||
|
Class="@EditButtonClass"
|
||
|
FillMode="@ThemeConstants.Button.FillMode.Clear"
|
||
|
OnClick="@ToggleEditMode"
|
||
|
Title="@Title">
|
||
|
@if (Value != null && (ValueType == typeof(bool) || !Value.Equals(default(T))) && !string.IsNullOrEmpty(Value.ToString()))
|
||
|
{
|
||
|
@GetFormattedValue()
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
<span class="@PlaceholderClass">@Placeholder</span>
|
||
|
}
|
||
|
@if (ShouldRenderEditIcon)
|
||
|
{
|
||
|
<TelerikSvgIcon Icon="@SvgIcon.Pencil" Class="@EditIconClass" />
|
||
|
}
|
||
|
</TelerikButton>
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
@GetFormattedValue()
|
||
|
}
|
||
|
</span>
|
||
|
@code {
|
||
|
#region Parameters
|
||
|
|
||
|
/// <summary>
|
||
|
/// A CSS class that can apply custom styles.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public string? Class { get; set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// The format string that will be used to display the component <see cref="Value" /> in read and edit mode.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public string? DisplayFormat { get; set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// The label that will show if the component <see cref="Value" /> matches the default one for the type.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public string Placeholder { get; set; } = string.Empty;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Sets if the user can edit the component <see cref="Value" />.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public bool ReadOnly { get; set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// Defines when the edit icon shows - always, on hover or never. The default value is <see cref="InPlaceEditorShowIcons.Always" />.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public InPlaceEditorShowIcons ShowIcons { get; set; } = InPlaceEditorShowIcons.Always;
|
||
|
|
||
|
/// <summary>
|
||
|
/// The tooltip content that shows in read mode.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public string Title { get; set; } = "Edit Value";
|
||
|
|
||
|
/// <summary>
|
||
|
/// The editable component value. The supported types include <see cref="string" />, signed numeric types,
|
||
|
/// <see cref="DateTime" />, <see cref="TimeOnly" />, and <see cref="bool" />
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public T? Value { get; set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// An event that fires when the user edits the component <see cref="Value" />.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public EventCallback<T> ValueChanged { get; set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// The <see cref="Expression"/> used for Form validation.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
public Expression<Func<T>>? ValueExpression { get; set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// The width style of the edit component (DatePicker, NumericTextBox, TextBox, TimePicker). Not relevant to checkboxes.
|
||
|
/// </summary>
|
||
|
[Parameter]
|
||
|
|
||
|
public string Width { get; set; } = string.Empty;
|
||
|
|
||
|
#endregion Parameters
|
||
|
|
||
|
#region Constants
|
||
|
|
||
|
private const string InPlaceEditorClass = "in-place-editor";
|
||
|
|
||
|
private const string CheckBoxClass = "in-place-checkbox";
|
||
|
|
||
|
private const string ButtonClass = "in-place-button";
|
||
|
|
||
|
private const string EditButtonClass = $"{ButtonClass} in-place-edit-button";
|
||
|
|
||
|
private const string IconClass = "in-place-icon";
|
||
|
|
||
|
private const string IconHoverableClass = $"{IconClass} in-place-hoverable-icon";
|
||
|
|
||
|
private const string InputClass = "in-place-input";
|
||
|
|
||
|
private const string PlaceholderClass = "in-place-placeholder";
|
||
|
|
||
|
#endregion Constants
|
||
|
|
||
|
#region Properties
|
||
|
|
||
|
private readonly string DataId = Guid.NewGuid().ToString();
|
||
|
|
||
|
private T? OriginalEditValue { get; set; }
|
||
|
|
||
|
private Type ValueType { get; set; } = typeof(string);
|
||
|
|
||
|
private InPlaceEditorType ValueEditorType { get; set; } = InPlaceEditorType.TextBox;
|
||
|
|
||
|
private bool IsInEditMode { get; set; }
|
||
|
|
||
|
private bool ShouldFocusEditor { get; set; }
|
||
|
|
||
|
private bool ShouldRenderEditIcon => ShowIcons != InPlaceEditorShowIcons.Never || GetFormattedValue().Length == 0;
|
||
|
|
||
|
private bool ShouldWaitForCancel { get; set; }
|
||
|
|
||
|
private bool ShouldFocusEditButton { get; set; }
|
||
|
|
||
|
private string ClassToRender => string.Format("{0} {1}", InPlaceEditorClass, Class);
|
||
|
|
||
|
private string EditIconClass => ShowIcons == InPlaceEditorShowIcons.Hover && GetFormattedValue().Length > 0 ? IconHoverableClass : IconClass;
|
||
|
|
||
|
#endregion Properties
|
||
|
|
||
|
#region Telerik Components
|
||
|
|
||
|
private TelerikButton? EditButtonRef { get; set; }
|
||
|
private TelerikTextBox? TextBoxRef { get; set; }
|
||
|
private TelerikNumericTextBox<T>? NumericTextBoxRef { get; set; }
|
||
|
private TelerikDatePicker<T>? DatePickerRef { get; set; }
|
||
|
private TelerikTimePicker<T>? TimePickerRef { get; set; }
|
||
|
private TelerikCheckBox<bool>? CheckBoxRef { get; set; }
|
||
|
|
||
|
private async Task OnEditorValueChanged(object newValue)
|
||
|
{
|
||
|
Value = (T)newValue;
|
||
|
if (ValueChanged.HasDelegate)
|
||
|
{
|
||
|
await ValueChanged.InvokeAsync((T)newValue);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endregion Telerik Components
|
||
|
|
||
|
#region Methods
|
||
|
|
||
|
private void OnEditorChange(object newValue)
|
||
|
{
|
||
|
if (!ShouldRenderEditIcon)
|
||
|
{
|
||
|
IsInEditMode = false;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ShouldWaitForCancel = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void OnSaveButtonClick()
|
||
|
{
|
||
|
IsInEditMode = false;
|
||
|
ShouldFocusEditButton = true;
|
||
|
}
|
||
|
|
||
|
private void OnCancelButtonClick()
|
||
|
{
|
||
|
Value = OriginalEditValue;
|
||
|
ShouldFocusEditButton = true;
|
||
|
IsInEditMode = false;
|
||
|
}
|
||
|
|
||
|
private async Task OnSpanKeyDown(KeyboardEventArgs args)
|
||
|
{
|
||
|
if (args.Key == "Escape")
|
||
|
{
|
||
|
Value = OriginalEditValue;
|
||
|
IsInEditMode = false;
|
||
|
if (ValueChanged.HasDelegate)
|
||
|
{
|
||
|
await ValueChanged.InvokeAsync(Value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (args.Key == "Enter")
|
||
|
{
|
||
|
IsInEditMode = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void OnSpanFocusIn(FocusEventArgs args)
|
||
|
{
|
||
|
ShouldWaitForCancel = false;
|
||
|
}
|
||
|
|
||
|
private string GetEditorWidth(InPlaceEditorType editorType)
|
||
|
{
|
||
|
if (!string.IsNullOrEmpty(Width))
|
||
|
{
|
||
|
return Width;
|
||
|
}
|
||
|
switch (editorType)
|
||
|
{
|
||
|
case InPlaceEditorType.DatePicker:
|
||
|
return $"{Math.Max(GetFormattedValue().Length, 9)}em".Replace(",", ".");
|
||
|
case InPlaceEditorType.NumericTextBox:
|
||
|
return $"{Math.Max(GetFormattedValue().Length * .6 + 3, 7)}em".Replace(",", ".");
|
||
|
case InPlaceEditorType.TextBox:
|
||
|
return $"{Math.Max(GetFormattedValue().Length * .75, 7)}em".Replace(",", ".");
|
||
|
case InPlaceEditorType.TimePicker:
|
||
|
return $"{GetFormattedValue().Length + 2}em".Replace(",", ".");
|
||
|
default:
|
||
|
throw new ArgumentOutOfRangeException(nameof(InPlaceEditorType));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void ToggleEditMode()
|
||
|
{
|
||
|
IsInEditMode = !IsInEditMode;
|
||
|
if (IsInEditMode)
|
||
|
{
|
||
|
OriginalEditValue = Value;
|
||
|
ShouldFocusEditor = true;
|
||
|
}
|
||
|
}
|
||
|
private string GetFormattedValue()
|
||
|
{
|
||
|
if (IsNumericValueType())
|
||
|
{
|
||
|
return Convert.ToDouble(Value).ToString(DisplayFormat);
|
||
|
}
|
||
|
else if ((ValueType == typeof(DateTime) || ValueType == typeof(DateOnly)) && Value != null)
|
||
|
{
|
||
|
return Convert.ToDateTime(Value).ToString(DisplayFormat);
|
||
|
}
|
||
|
else if (ValueType == typeof(TimeOnly))
|
||
|
{
|
||
|
var success = TimeOnly.TryParse(Value?.ToString() ?? string.Empty, CultureInfo.InvariantCulture, out TimeOnly timeOnly);
|
||
|
if (success)
|
||
|
{
|
||
|
return timeOnly.ToString(DisplayFormat);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return string.Empty;
|
||
|
}
|
||
|
}
|
||
|
else if (ValueType == typeof(bool))
|
||
|
{
|
||
|
return Convert.ToBoolean(Value).ToString();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return Value?.ToString() ?? string.Empty;
|
||
|
}
|
||
|
}
|
||
|
private void GetValueType()
|
||
|
{
|
||
|
if (Value == null)
|
||
|
{
|
||
|
Type? nullableType = Nullable.GetUnderlyingType(typeof(T));
|
||
|
if (nullableType != null)
|
||
|
{
|
||
|
ValueType = nullableType;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
throw new ArgumentNullException(nameof(Value));
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
ValueType = Value.GetType();
|
||
|
}
|
||
|
if (IsNumericValueType())
|
||
|
{
|
||
|
ValueEditorType = InPlaceEditorType.NumericTextBox;
|
||
|
}
|
||
|
else if (ValueType == typeof(DateTime) || ValueType == typeof(DateOnly))
|
||
|
{
|
||
|
ValueEditorType = InPlaceEditorType.DatePicker;
|
||
|
}
|
||
|
else if (ValueType == typeof(TimeOnly))
|
||
|
{
|
||
|
ValueEditorType = InPlaceEditorType.TimePicker;
|
||
|
}
|
||
|
else if (ValueType == typeof(bool))
|
||
|
{
|
||
|
ValueEditorType = InPlaceEditorType.CheckBox;
|
||
|
}
|
||
|
}
|
||
|
private bool IsNumericValueType()
|
||
|
{
|
||
|
return
|
||
|
ValueType == typeof(int) ||
|
||
|
ValueType == typeof(short) ||
|
||
|
ValueType == typeof(byte) ||
|
||
|
ValueType == typeof(long) ||
|
||
|
ValueType == typeof(float) ||
|
||
|
ValueType == typeof(double) ||
|
||
|
ValueType == typeof(decimal);
|
||
|
}
|
||
|
|
||
|
#endregion Methods
|
||
|
|
||
|
#region Life Cycle Events
|
||
|
|
||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||
|
{
|
||
|
if (ShouldFocusEditor)
|
||
|
{
|
||
|
ShouldFocusEditor = false;
|
||
|
await Task.Delay(100);
|
||
|
|
||
|
if (NumericTextBoxRef != null)
|
||
|
await NumericTextBoxRef.FocusAsync();
|
||
|
if (DatePickerRef != null)
|
||
|
await DatePickerRef.FocusAsync();
|
||
|
if (TimePickerRef != null)
|
||
|
await TimePickerRef.FocusAsync();
|
||
|
if (CheckBoxRef != null)
|
||
|
await CheckBoxRef.FocusAsync();
|
||
|
if (TextBoxRef != null)
|
||
|
await TextBoxRef.FocusAsync();
|
||
|
}
|
||
|
|
||
|
if (ShouldFocusEditButton)
|
||
|
{
|
||
|
ShouldFocusEditButton = false;
|
||
|
await Task.Delay(100);
|
||
|
if (EditButtonRef != null)
|
||
|
{
|
||
|
await EditButtonRef.FocusAsync();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (ShouldWaitForCancel)
|
||
|
{
|
||
|
await Task.Delay(100);
|
||
|
if (ShouldWaitForCancel)
|
||
|
{
|
||
|
ShouldWaitForCancel = false;
|
||
|
IsInEditMode = false;
|
||
|
StateHasChanged();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected override void OnInitialized()
|
||
|
{
|
||
|
GetValueType();
|
||
|
|
||
|
base.OnInitialized();
|
||
|
}
|
||
|
|
||
|
#endregion Life Cycle Events
|
||
|
|
||
|
public enum InPlaceEditorType
|
||
|
{
|
||
|
CheckBox,
|
||
|
DatePicker,
|
||
|
TimePicker,
|
||
|
NumericTextBox,
|
||
|
TextBox
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
````InPlaceEditor.razor.css
|
||
|
/*
|
||
|
This .razor.css file relies on Blazor CSS isolation, which in turn requires a YourAppName.styles.css file in App.razor.
|
||
|
Make sure that the browser doesn't load an old cached version of this file, otherwise you may not see the InPlaceEditor styles.
|
||
|
A symptom of this problem are persistent icons when ShowIcons="InPlaceEditorShowIcons.Hover".
|
||
|
*/
|
||
|
|
||
|
.in-place-editor {
|
||
|
display: inline-flex;
|
||
|
font-family: monospace;
|
||
|
}
|
||
|
|
||
|
::deep .in-place-checkbox {
|
||
|
margin-inline: 1em;
|
||
|
}
|
||
|
|
||
|
::deep .in-place-button,
|
||
|
::deep .in-place-icon {
|
||
|
color: inherit;
|
||
|
}
|
||
|
|
||
|
::deep .in-place-icon {
|
||
|
margin-inline-start: .5em;
|
||
|
}
|
||
|
|
||
|
::deep .in-place-hoverable-icon {
|
||
|
display: none;
|
||
|
}
|
||
|
|
||
|
::deep .in-place-edit-button:hover {
|
||
|
background-color: var(--kendo-color-base) !important;
|
||
|
}
|
||
|
|
||
|
::deep .in-place-edit-button:hover .in-place-hoverable-icon {
|
||
|
display: inline-flex;
|
||
|
}
|
||
|
|
||
|
::deep .in-place-placeholder {
|
||
|
color: var(--kendo-color-secondary);
|
||
|
}
|
||
|
````
|
||
|
````InPlaceEditorShowIcons.cs
|
||
|
namespace YourAppName.Models
|
||
|
{
|
||
|
public enum InPlaceEditorShowIcons
|
||
|
{
|
||
|
Always,
|
||
|
Hover,
|
||
|
Never
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
|
||
|
## See Also
|
||
|
|
||
|
* [Button Overview]({%slug components/button/overview%})
|
||
|
* [CheckBox Overview]({%slug checkbox-overview%})
|
||
|
* [DatePicker Overview]({%slug components/datepicker/overview%})
|
||
|
* [NumericTextBox Overview]({%slug components/numerictextbox/overview%})
|
||
|
* [TextBox Overview]({%slug components/textbox/overview%})
|
||
|
* [TimePicker Overview]({%slug components/timepicker/overview%})
|