Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/BootstrapBlazor.Server/Components/Samples/Toasts.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@page "/toast"
@inject IStringLocalizer<Toasts> Localizer
@inject ToastService ToastService
@inject IJSRuntime JSRuntime

<h3>@Localizer["ToastsTitle"]</h3>

Expand Down Expand Up @@ -51,6 +50,10 @@ private ToastService? ToastService { get; set; }
<Button Icon="fa-solid fa-triangle-exclamation" OnClick="@OnPreventClick" Text="@Localizer["ToastsPreventText"]"></Button>
</DemoBlock>

<DemoBlock Title="@Localizer["ToastsAsyncTitle"]" Introduction="@Localizer["ToastsAsyncIntro"]" Name="Async" ShowCode="false">
<Button IsAsync="true" Icon="fa-solid fa-triangle-exclamation" OnClick="@OnAsyncClick" Text="@Localizer["ToastsAsyncText"]"></Button>
</DemoBlock>

<DemoBlock Title="@Localizer["ToastsCloseTitle"]" Introduction="@Localizer["ToastsCloseIntro"]" Name="Close" ShowCode="false">
<Button Icon="fa-solid fa-triangle-exclamation" OnClick="@OnNotAutoHideClick" Text="@Localizer["ToastsCloseNotificationText"]"></Button>
<ConsoleLogger @ref="Logger"></ConsoleLogger>
Expand Down
59 changes: 42 additions & 17 deletions src/BootstrapBlazor.Server/Components/Samples/Toasts.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ public sealed partial class Toasts
[Inject, NotNull]
private IOptions<BootstrapBlazorOptions>? Options { get; set; }

private int _delayTs => Options.Value.ToastDelay / 1000;
private int DelayTs => Options.Value.ToastDelay / 1000;

private readonly ToastOption _option = new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider creating a new ToastOption instance for each toast step instead of reusing and mutating a shared field.

Consider avoiding the shared, mutable _option and instead build a fresh ToastOption for each step (or extract a small factory helper). This keeps each toast’s state isolated and makes the flow easier to follow.

For example, you can extract a helper:

private ToastOption CreateStep(string content, ToastCategory category,
                               bool isAutoHide = true, int delay = 3000, bool forceDelay = false)
{
  return new ToastOption {
    Title       = Localizer["ToastsAsyncDemoTitle"],
    Content     = content,
    Category    = category,
    IsAutoHide  = isAutoHide,
    Delay       = delay,
    ForceDelay  = forceDelay
  };
}

Then rewrite OnAsyncClick without mutating a field:

private async Task OnAsyncClick()
{
  // Step 1
  var first = CreateStep(Localizer["ToastsAsyncDemoStep1Text"],
                         ToastCategory.Information,
                         isAutoHide: false,
                         delay: 3000,
                         forceDelay: true);
  await ToastService.Show(first);
  await Task.Delay(first.Delay);

  // Step 2
  var second = CreateStep(Localizer["ToastsAsyncDemoStep2Text"],
                          ToastCategory.Information,
                          isAutoHide: true);
  await ToastService.Show(second);
  await Task.Delay(2000);

  // Step 3
  var third = CreateStep(Localizer["ToastsAsyncDemoStep3Text"],
                         ToastCategory.Success);
  await ToastService.Show(third);
}

This removes the mutable _option and makes each toast instantiation self-contained.


/// <summary>
/// OnInitialized
Expand All @@ -46,10 +48,10 @@ protected override void OnInitialized()
{
base.OnInitialized();

Options1 = new ToastOption { Title = "Save data", IsAutoHide = false, Content = $"Save data successfully, automatically close after {_delayTs} seconds" };
Options2 = new ToastOption { Category = ToastCategory.Error, Title = "Save data", IsAutoHide = false, Content = $"Save data successfully, automatically close after {_delayTs} seconds" };
Options3 = new ToastOption { Category = ToastCategory.Information, Title = "Prompt information", IsAutoHide = false, Content = $"Information prompt pop-up window, automatically closes after {_delayTs} seconds" };
Options4 = new ToastOption { Category = ToastCategory.Warning, Title = "Warning message", IsAutoHide = false, Content = $"Information prompt pop-up window, automatically closes after {_delayTs} seconds" };
Options1 = new ToastOption { Title = "Save data", IsAutoHide = false, Content = $"Save data successfully, automatically close after {DelayTs} seconds" };
Options2 = new ToastOption { Category = ToastCategory.Error, Title = "Save data", IsAutoHide = false, Content = $"Save data successfully, automatically close after {DelayTs} seconds" };
Options3 = new ToastOption { Category = ToastCategory.Information, Title = "Prompt information", IsAutoHide = false, Content = $"Information prompt pop-up window, automatically closes after {DelayTs} seconds" };
Options4 = new ToastOption { Category = ToastCategory.Warning, Title = "Warning message", IsAutoHide = false, Content = $"Information prompt pop-up window, automatically closes after {DelayTs} seconds" };

ToastContainer = Root.ToastContainer;
}
Expand All @@ -62,18 +64,41 @@ await ToastService.Show(new ToastOption()
PreventDuplicates = true,
Category = ToastCategory.Success,
Title = "Successfully saved",
Content = $"Save data successfully, automatically close after {_delayTs} seconds"
Content = $"Save data successfully, automatically close after {DelayTs} seconds"
});
}

private async Task OnAsyncClick()
{
_option.Title = Localizer["ToastsAsyncDemoTitle"];
_option.ForceDelay = true;
_option.IsAutoHide = false;
_option.Delay = 3000;
_option.Content = Localizer["ToastsAsyncDemoStep1Text"];
_option.Category = ToastCategory.Information;
await ToastService.Show(_option);

await Task.Delay(3000);
_option.Content = Localizer["ToastsAsyncDemoStep2Text"];
_option.IsAutoHide = true;
_option.Category = ToastCategory.Information;
await ToastService.Show(_option);

await Task.Delay(2000);
_option.Content = Localizer["ToastsAsyncDemoStep3Text"];
_option.Category = ToastCategory.Success;

await ToastService.Show(_option);
}

private async Task OnSuccessClick()
{
ToastContainer.SetPlacement(Placement.BottomEnd);
await ToastService.Show(new ToastOption()
{
Category = ToastCategory.Success,
Title = "Successfully saved",
Content = $"Save data successfully, automatically close after {_delayTs} seconds"
Content = $"Save data successfully, automatically close after {DelayTs} seconds"
});
}

Expand All @@ -84,7 +109,7 @@ await ToastService.Show(new ToastOption()
{
Category = ToastCategory.Error,
Title = "Failed to save",
Content = $"Failed to save data, automatically closes after {_delayTs} seconds"
Content = $"Failed to save data, automatically closes after {DelayTs} seconds"
});
}

Expand All @@ -95,7 +120,7 @@ await ToastService.Show(new ToastOption()
{
Category = ToastCategory.Information,
Title = "Notification",
Content = $"The system adds new components, it will automatically shut down after {_delayTs} seconds"
Content = $"The system adds new components, it will automatically shut down after {DelayTs} seconds"
});
}

Expand All @@ -106,7 +131,7 @@ await ToastService.Show(new ToastOption()
{
Category = ToastCategory.Warning,
Title = "Warning",
Content = $"If the system finds abnormality, please deal with it in time, and it will automatically shut down after {_delayTs} seconds"
Content = $"If the system finds abnormality, please deal with it in time, and it will automatically shut down after {DelayTs} seconds"
});
}

Expand Down Expand Up @@ -135,7 +160,7 @@ await ToastService.Show(new ToastOption()
{
Category = ToastCategory.Warning,
ShowHeader = false,
Content = $"The system adds new components, it will automatically shut down after {_delayTs} seconds"
Content = $"The system adds new components, it will automatically shut down after {DelayTs} seconds"
});
}

Expand All @@ -146,7 +171,7 @@ await ToastService.Show(new ToastOption()
{
Category = ToastCategory.Information,
HeaderTemplate = RenderHeader,
Content = $"The system adds new components, it will automatically shut down after {_delayTs} seconds"
Content = $"The system adds new components, it will automatically shut down after {DelayTs} seconds"
});
}

Expand All @@ -157,7 +182,7 @@ await ToastService.Show(new ToastOption()
{
Category = ToastCategory.Information,
Title = "Notification",
Content = $"<b>Toast</b> The component has changed position, it will automatically shut down after {_delayTs} seconds"
Content = $"<b>Toast</b> The component has changed position, it will automatically shut down after {DelayTs} seconds"
});
}

Expand All @@ -176,31 +201,31 @@ private AttributeItem[] GetAttributes() =>
Name = "Title",
Description = Localizer["ToastsAttrTitle"],
Type = "string",
ValueList = "",
ValueList = "",
DefaultValue = ""
},
new()
{
Name = "Content",
Description = Localizer["ToastsAttrContent"],
Type = "string",
ValueList = "",
ValueList = "",
DefaultValue = ""
},
new()
{
Name = "Delay",
Description = Localizer["ToastsAttrDelay"],
Type = "int",
ValueList = "",
ValueList = "",
DefaultValue = "4000"
},
new()
{
Name = "IsAutoHide",
Description = Localizer["ToastsAttrIsAutoHide"],
Type = "boolean",
ValueList = "",
ValueList = "",
DefaultValue = "true"
},
new()
Expand Down
7 changes: 7 additions & 0 deletions src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,14 @@
"ToastsPreventTitle": "Prevent Duplicates",
"ToastsPreventIntro": "By setting <code>PreventDuplicates=\"true\"</code> to repeatedly click the button below, only one pop-up window will appear",
"ToastsPreventText": "Prevent Duplicates",
"ToastsAsyncTitle": "Async notification",
"ToastsAsyncIntro": "By setting <code>IsAsync</code>, the pop-up window is displayed, the thread is blocked, and after clicking the close button, the subsequent code continues to execute",
"ToastsAsyncDemoTitle": "Async notification",
"ToastsAsyncDemoStep1Text": "Packing documents, please wait...",
"ToastsAsyncDemoStep2Text": "Packaging completed, downloading...",
"ToastsAsyncDemoStep3Text": "Download completed, close the window automatically",
"ToastsWarning": "Warning notice",
"ToastsAsyncText": "AsyncNotify",
"ToastsCloseTitle": "Toast is closed manually",
"ToastsCloseIntro": "It will not close automatically, you need to manually click the close button. You can customize the event after closing the pop-up window by setting<code>OnCloseAsync</code>callback delegate</code>",
"ToastsCloseNotificationText": "success notification",
Expand Down
7 changes: 7 additions & 0 deletions src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,14 @@
"ToastsPreventTitle": "阻止重复",
"ToastsPreventIntro": "通过设置 <code>PreventDuplicates=\"true\"</code> 重复点击下方按钮时,仅弹窗一次",
"ToastsPreventText": "阻止重复",
"ToastsAsyncTitle": "线程阻塞通知",
"ToastsAsyncIntro": "通过设置 <code>IsAsync</code> 弹窗显示后,线程阻塞,点击关闭按钮后,继续执行后续代码",
"ToastsAsyncDemoTitle": "异步通知",
"ToastsAsyncDemoStep1Text": "正在打包文档,请稍等...",
"ToastsAsyncDemoStep2Text": "打包完成,正在下载...",
"ToastsAsyncDemoStep3Text": "下载完成,自动关窗",
"ToastsWarning": "警告通知",
"ToastsAsyncText": "线程阻塞通知示例",
"ToastsCloseTitle": "Toast 手动关闭",
"ToastsCloseIntro": "不会自动关闭,需要人工点击关闭按钮,可通过设置 <code>OnCloseAsync</code> 回调委托自定义关闭弹窗后事件",
"ToastsCloseNotificationText": "成功通知",
Expand Down
23 changes: 17 additions & 6 deletions src/BootstrapBlazor/Components/Toast/Toast.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,27 @@ public partial class Toast
/// <summary>
/// 获得/设置 弹出框自动关闭时长
/// </summary>
protected string? DelayString => Options.IsAutoHide ? Convert.ToString(Options.Delay) : null;
private string? DelayString => Options.IsAutoHide ? Options.Delay.ToString() : null;

/// <summary>
/// 获得/设置 是否开启动画效果
/// 获得/设置 是否开启动画效果
/// </summary>
protected string? AnimationString => Options.Animation ? null : "false";
private string? AnimationString => Options.Animation ? null : "false";

/// <summary>
/// 获得/设置 ToastOption 实例
/// </summary>
[Parameter]
[NotNull]
#if NET6_0_OR_GREATER
[EditorRequired]
#endif
public ToastOption? Options { get; set; }

/// <summary>
/// 获得/设置 Toast 实例
/// </summary>
/// <value></value>
[CascadingParameter]
protected ToastContainer? ToastContainer { get; set; }
private ToastContainer? ToastContainer { get; set; }

[Inject]
[NotNull]
Expand Down Expand Up @@ -100,6 +98,19 @@ protected override void OnParametersSet()
Options.ErrorIcon ??= IconTheme.GetIconByKey(ComponentIcons.ToastErrorIcon);
}

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);

if (!firstRender)
{
await InvokeVoidAsync("update", Id);
}
}

/// <summary>
/// <inheritdoc/>
/// </summary>
Expand Down
21 changes: 20 additions & 1 deletion src/BootstrapBlazor/Components/Toast/Toast.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function init(id, invoke, callback) {
return toast.toast._config.autohide
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Event handler for 'transitionend' may be attached multiple times.

Attaching a new listener on each update can cause duplicate handlers, triggering toast.hide() repeatedly and leaking memory. Consider removing existing handlers or using a one-time listener.

}
Data.set(id, toast)
Data.set(id, toast);

if (toast.showProgress()) {
toast.progressElement = toast.element.querySelector('.toast-progress')
Expand All @@ -31,6 +31,25 @@ export function init(id, invoke, callback) {
toast.toast.show()
}

export function update(id) {
const t = Data.get(id);
const { element, toast } = t;
const autoHide = element.getAttribute('data-bs-autohide') !== 'false';
if(autoHide) {
const delay = parseInt(element.getAttribute('data-bs-delay'));
const progressElement = element.querySelector('.toast-progress');

toast._config.autohide = autoHide;
toast._config.delay = delay;

progressElement.style.width = '100%';
progressElement.style.transition = `width linear ${delay / 1000}s`;
EventHandler.on(progressElement, 'transitionend', e => {
toast.hide();
});
}
}

export function dispose(id) {
const toast = Data.get(id)
Data.remove(id)
Expand Down
7 changes: 6 additions & 1 deletion src/BootstrapBlazor/Components/Toast/ToastContainer.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ private async Task Show(ToastOption option)
return;
}
}
Toasts.Add(option);

// support update content
if (!Toasts.Contains(option))
{
Toasts.Add(option);
}
await InvokeAsync(StateHasChanged);
}

Expand Down