Skip to content

Commit

Permalink
[dev-v5] Add FluentTextInput (#2930)
Browse files Browse the repository at this point in the history
* Add FluentInputBase and FluentInputImmediateBase

* Add FluentTextInput

* Refactoring FluentInputBase (removing CurrentValue)

* Add Base Unit Tests

* Add Label

* Add Appearance and sample

* Add Immediate examples

* Add missing properties

* Add ObserveAttributeChanges to detect and apply the Value attribute

* Fix the API comments

* Fix Global Unit Tests

* Add new tests

* Fix Unit Tests

* Add Unit Tests for Extension methods

* [dev-v5] Use standard `InputBase` class (#2933)

* Add FluentJSModule

* Update the doc

* Replace FluentInputBase with inheritance

* - Fix runtime error (caused by using only Value)
- Add 'Lab' page to do quick experiments
- Exlude YT video because of constant script error
- Adapt CopySources to use net9.0 TFM

---------

Co-authored-by: Denis Voituron <[email protected]>
Co-authored-by: Vincent Baaij <[email protected]>

* Refactoring

* Add default ValueExpression

---------

Co-authored-by: Denis Voituron (MSFT) <[email protected]>
Co-authored-by: Vincent Baaij <[email protected]>
  • Loading branch information
3 people authored Nov 15, 2024
1 parent 1704a59 commit ff47fd8
Show file tree
Hide file tree
Showing 41 changed files with 1,311 additions and 128 deletions.
12 changes: 12 additions & 0 deletions examples/Demo/FluentUI.Demo.Client/DebugPages/EditFormPage.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@page "/Debug/EditForm"

<h1>Just a simple page to do some experimenting</h1>

<InputText @bind-Value="@MyValue" />
<FluentTextInput Value="@MyValue" />

MyValue: @MyValue

@code {
public string MyValue { get; set; } = "Test";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<FluentTextInput Appearance="@TextInputAppearance.Outline" Label="Outline" Placeholder="Outline" @bind-Value="@Value" />
<FluentTextInput Appearance="@TextInputAppearance.Underline" Label="Underline" Placeholder="Underline" @bind-Value="@Value" />
<FluentTextInput Appearance="@TextInputAppearance.FilledLighter" Label="FilledLighter" Placeholder="FilledLighter" @bind-Value="@Value" />
<FluentTextInput Appearance="@TextInputAppearance.FilledDarker" Label="FilledDarker" Placeholder="FilledDarker" @bind-Value="@Value" />
</div>

<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<FluentTextInput Size="@TextInputSize.Small" Label="Small" Placeholder="Small" @bind-Value="@Value" />
<FluentTextInput Size="@TextInputSize.Medium" Label="Medium" Placeholder="Medium" @bind-Value="@Value" />
<FluentTextInput Size="@TextInputSize.Large" Label="Large" Placeholder="Large" @bind-Value="@Value" />
</div>

<div>
Value = @Value
</div>

@code
{
string Value = "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<FluentTextInput Appearance="@TextInputAppearance.Outline"
Placeholder="Updated after 400ms"
@bind-Value="@Value"
@bind-Value:after="@(() => Console.WriteLine($"TextInput updated to '{Value}'.") )"
Immediate="true"
ImmediateDelay="400" />
</div>

<div>
Value = @Value
</div>

@code
{
string Value = "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<FluentTextInput Label="Company Url" @bind-Value="@_sometext1">
<StartTemplate>
<fluent-label>https://</fluent-label>
</StartTemplate>
<EndTemplate>
<fluent-label>.com</fluent-label>
</EndTemplate>
</FluentTextInput>

<FluentTextInput Label="Email" TextFieldType="TextInputType.Email" @bind-Value="@_sometext2" Style="width: 300px;">
<StartTemplate>
<FluentIcon Value="@(new Icons.Regular.Size20.Mail())" />
</StartTemplate>
</FluentTextInput>
</div>

@code {
private string _sometext1 = "microsoft";
private string _sometext2 = "[email protected]";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
<FluentTextInput Required="true" Label="Required" Placeholder="Required" @bind-Value="@Value" />
<FluentTextInput Disabled="true" Label="Disabled" Placeholder="Disabled" @bind-Value="@Value" />
<FluentTextInput ReadOnly="true" Label="ReadOnly" Placeholder="ReadOnly" @bind-Value="@Value" />
</div>

<div>
Value = @Value
</div>

@code
{
string Value = "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: TextInput
route: /TextInput
---

# TextInput

A **FluentTextInput** component enables a user to enter text into an app.
It's typically used to capture a single line of text.
The text displays on the screen in a simple, uniform format.

## Appearance

The apparent style of a text input can be changed by setting the `Appearance` property, but also by setting the `Size` property.

You can also add a label to the text input by setting the `Label` property and a placeholder by setting the `Placeholder` property.
The label will be automatically positioned above the text input, and the placeholder will be displayed inside the text input.

We recommand to use a spacing of 24px between text fields and other components.

{{ TextInputAppearances }}

Although not recommended by FluentUI, an input can be rendered inline with text using a style attribute.

```
<div>
Name:
<FluentTextInput Style="display: inline-block;" />
</div>
```

## Binding with ImmediateDelay

In some cases, you may want to bind the value of the text input to a property of a model
and update the model immediately after the user types a character. But you may also want to delay the update.
This can be achieved by setting the `Immediate` and the optional `ImmediateDelay` properties.

{{ TextInputImmediate }}

## States

A text input can be in different states, such as `Disabled`, `ReadOnly`, and `Required`.

{{ TextInputState }}

## Prefix and Suffix

You can use the `StartTemplate` and `EndTemplate` properties to add a prefix or a suffix to the text input
as `https://` and `.com` or an icon.

These templates are automatically positioned with a small margin between the text entered and the prefix/suffix.
You cannot therefore fill the entire background of these templates, with a colour for example.

{{ TextInputPrefixSuffix }}

## API FluentTextInput

{{ API Type=FluentTextInput }}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ hidden: true
if (firstRender)
{
// Import the JavaScript module
var jsModule = await ImportJavaScriptModuleAsync(JAVASCRIPT_FILE);
var jsModule = await JSModule.ImportJavaScriptModuleAsync(JAVASCRIPT_FILE);
// Call a function from the JavaScript module
await jsModule.InvokeAsync<string>("Microsoft.FluentUI.Blazor.Button.MyFunction");
Expand Down
6 changes: 5 additions & 1 deletion examples/Demo/FluentUI.Demo.Client/Documentation/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ see the **"Getting Started"** section below.

## Introduction and getting started video

<iframe width="450" height="252" src="https://www.youtube.com/embed/lUZ5mrg2Q8k?si=Xv4_EJxP0Z_GFdLQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<!--
commented out because YT is constantly throwing JS errors
<iframe width="450" height="252" src="https://www.youtube.com/embed/lUZ5mrg2Q8k?si=Xv4_EJxP0Z_GFdLQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
-->
## Getting Started

By far the easiest way to get started is by using our templates. Setting them up is quick and easy.
See the [templates](/Templates) page for instructions and usage.

Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
</ItemGroup>

<!-- Copy all .razor files to .txt files in wwwroot\sources -->
<Target Name="CopySources" BeforeTargets="BeforeBuild" Condition="'$(TargetFramework)'=='net8.0'">
<Target Name="CopySources" BeforeTargets="BeforeBuild" Condition="'$(TargetFramework)'=='net9.0'">
<ItemGroup>
<Sources Include="$(ProjectDir)\Documentation\**\*.razor*" />
</ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion examples/Demo/FluentUI.Demo/Components/App.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html lang="en">
@{
var DemoRenderMode = new InteractiveWebAssemblyRenderMode(false);
//var DemoRenderMode = new InteractiveWebAssemblyRenderMode(false);
var DemoRenderMode = new InteractiveServerRenderMode(false);
}
<head>
<meta charset="utf-8" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
}

.doc-viewer ::deep .api-table td code {
font-weight: 500;
font-weight: 600;
background-color: rgb(243, 243, 243);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ public class CodeCommentsGenerator : IIncrementalGenerator
{
private static readonly string[] REGEX_CLEANUP =
[
"Microsoft\\.FluentUI\\.AspNetCore\\.Components\\.",
"FluentUI\\.Demo\\.Client\\."
@"Microsoft\.FluentUI\.AspNetCore\.Components\.",
@"FluentUI\.Demo\.Client\.",
@"\[\[.*?\]\]",
@"\[.*?\]"
];

public void Initialize(IncrementalGeneratorInitializationContext context)
Expand Down
1 change: 1 addition & 0 deletions spelling.dic
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ appsettings
blazor
cref
csproj
datalist
elementreference
evenodd
microsoft
Expand Down
81 changes: 18 additions & 63 deletions src/Core/Components/Base/FluentComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,67 +11,37 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
/// <summary>
/// Base class for FluentUI Blazor components.
/// </summary>
public abstract class FluentComponentBase : ComponentBase, IAsyncDisposable
public abstract class FluentComponentBase : ComponentBase, IAsyncDisposable, IFluentComponentBase
{
private IJSObjectReference? _jsModule;
private FluentJSModule? _jsModule;

/// <summary>
/// Gets the root path for the JavaScript files.
/// </summary>
protected const string JAVASCRIPT_ROOT = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/";

/// <summary>
/// Gets or sets a reference to the JavaScript runtime.
/// This property is injected by the Blazor framework.
/// </summary>
/// <summary />
[Inject]
protected virtual IJSRuntime JSRuntime { get; set; } = default!;
private IJSRuntime JSRuntime { get; set; } = default!;

/// <summary>
/// Gets the JavaScript module imported with the <see cref="ImportJavaScriptModuleAsync"/> method.
/// Gets the JavaScript module imported with the <see cref="FluentJSModule.ImportJavaScriptModuleAsync"/> method.
/// You need to call this method (in the `OnAfterRenderAsync` method) before using the module.
/// </summary>
protected virtual IJSObjectReference JSModule => _jsModule ?? throw new InvalidOperationException("You must call `ImportJavaScriptModuleAsync` method before accessing the JSModule property.");
internal FluentJSModule JSModule => _jsModule ??= new FluentJSModule(JSRuntime);

/// <summary>
/// Invoke the JavaScript runtime to import the JavaScript module.
/// </summary>
/// <param name="file">Name of the JavaScript file to import (e.g. JAVASCRIPT_ROOT + "Button/FluentButton.razor.js").</param>
/// <returns></returns>
protected virtual async Task<IJSObjectReference> ImportJavaScriptModuleAsync(string file)
{
_jsModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", file); // TO ADD: .FormatCollocatedUrl(LibraryConfiguration)
return _jsModule;
}

/// <summary>
/// Gets or sets the unique identifier.
/// The value will be used as the HTML <see href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id">global id attribute</see>.
/// </summary>
/// <inheritdoc />
[Parameter]
public virtual string? Id { get; set; }

/// <summary>
/// Gets or sets the CSS class names. If given, these will be included in the class attribute of the component.
/// </summary>
/// <inheritdoc />
[Parameter]
public virtual string? Class { get; set; }

/// <summary>
/// Gets or sets the in-line styles. If given, these will be included in the style attribute of the component.
/// </summary>
/// <inheritdoc />
[Parameter]
public virtual string? Style { get; set; }

/// <summary>
/// Gets or sets custom data, to attach any user data object to the component.
/// </summary>
/// <inheritdoc />
[Parameter]
public virtual object? Data { get; set; }

/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created element.
/// </summary>
/// <inheritdoc />
[Parameter(CaptureUnmatchedValues = true)]
public virtual IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }

Expand All @@ -81,34 +51,19 @@ protected virtual async Task<IJSObjectReference> ImportJavaScriptModuleAsync(str
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
[ExcludeFromCodeCoverage]
public async ValueTask DisposeAsync()
public virtual async ValueTask DisposeAsync()
{
await DisposeAsync(_jsModule);

if (_jsModule != null)
{
try
{
await _jsModule.DisposeAsync();
}
catch (Exception ex) when (ex is JSDisconnectedException ||
ex is OperationCanceledException)
{
// The JSRuntime side may routinely be gone already if the reason we're disposing is that
// the client disconnected. This is not an error.
}
}

GC.SuppressFinalize(this);
await JSModule.DisposeAsync();
}

/// <summary>
/// Dispose the <see cref="JSModule"/> object.
/// Dispose the <paramref name="jsModule"/> object.
/// </summary>
/// <param name="jsModule"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
protected virtual ValueTask DisposeAsync(IJSObjectReference? jsModule)
[ExcludeFromCodeCoverage]
protected virtual async ValueTask DisposeAsync(IJSObjectReference? jsModule)
{
return ValueTask.CompletedTask;
await JSModule.DisposeAsync(jsModule);
}
}
Loading

0 comments on commit ff47fd8

Please sign in to comment.