Blazor Nested Components - c#

I want to be able to take in a list of blazor components as a parameter in a declarative way.
Component 1:
#foreach(Component2 input in Inputs)
{
#input
}
#code{
[Parameter]
public List<Component2> Inputs {get; set;}
}
Component 2:
<h1>#Header</h1>
#code{
[Parameter]
public string Header {get; set;}
}
How I want to use it:
<Component1>
<Inputs>
<Component2 Header="hello" />
<Component2 Header="world" />
</Inputs>
</Component1>
Does anyone know if this is possible?
Thank you,
Travis

One option is to override BuildRenderTree. Here we define a MessagesComponent that is supplied with a list of MessageComponents, each of which has a single message:
// YourPage.Razor
<p>Here are some messages:</p>
<MessagesComponent MessageComponents="messageComponents" />
#code {
private readonly IEnumerable<MessageComponent> messageComponents = new List<MessageComponent>
{
new MessageComponent { Message = "Hello" },
new MessageComponent { Message = "World" }
};
}
// MessagesComponent.cs
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace WebApplication3.Pages
{
public class MessagesComponent : ComponentBase
{
[Parameter]
public IEnumerable<MessageComponent> MessageComponents { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
// Render each component.
foreach (var component in MessageComponents)
{
builder.OpenComponent(0, typeof(MessageComponent));
builder.AddAttribute(1, nameof(MessageComponent.Message), component.Message);
builder.CloseComponent();
}
}
}
}
// MessageComponent.razor
#using Microsoft.AspNetCore.Components.Rendering
<p>This message says "#Message"</p>
#code {
[Parameter]
public string Message { get; set; }
}
This outputs like so:

While digging around the web I found a link to a Blazor University page that described this perfectly.
https://blazor-university.com/templating-components-with-renderfragements/creating-a-tabcontrol/
https://github.com/mrpmorris/blazor-university/tree/master/src/TemplatedComponents/CreatingATabControl
The gist of the solution is you send a reference of the parent to the child component. Then when the child is initialized you tell the parent to add the child to the list of components.
Parent Component:
<CascadingValue Value="this">
<div class="btn-group" role="group">
#foreach (Tab tabPage in Pages)
{
<button type="button"
class="btn #GetButtonClass(tabPage)"
#onclick=#( () => ActivatePage(tabPage) )>
#tabPage.Text
</button>
}
</div>
#ChildContent
</CascadingValue>
#code {
// Next line is needed so we are able to add <TabPage> components inside
[Parameter]
public RenderFragment ChildContent { get; set; }
public Tab ActivePage { get; set; }
List<Tab> Pages = new List<Tab>();
internal void AddPage(Tab tabPage)
{
Pages.Add(tabPage);
if (Pages.Count == 1)
ActivePage = tabPage;
StateHasChanged();
}
string GetButtonClass(Tab page)
{
return page == ActivePage ? "btn-primary" : "btn-secondary";
}
void ActivatePage(Tab page)
{
ActivePage = page;
}
}
Child Component:
#if (Parent.ActivePage == this)
{
#ChildContent
}
#code {
[CascadingParameter]
private Tabs Parent { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public string Text { get; set; }
protected override void OnInitialized()
{
if (Parent == null)
throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl");
base.OnInitialized();
Parent.AddPage(this);
}
}
Usage:
<ParentComponent>
<ChildComponent Text="Tab 1">
<h1>This is a test</h1>
</ChildComponent >
<ChildComponent Text="2">
<h3>This is a test</h3>
</ChildComponent >
<ChildComponent Text="3 Tab">
<h5>This is a test</h5>
</ChildComponent >
</ParentComponent>

Here's a simplistic but more generic way to achieve what you want. It renders three instances of your HeaderComponent and one Counter.
#page "/"
<h3>MultiHeader</h3>
#MyComponents
#code {
private List<Tuple<Type, Dictionary<string, object>>> _components = new List<Tuple<Type, Dictionary<string, object>>>();
protected override void OnInitialized()
{
_components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(HeaderComponent), new Dictionary<string, object>() { { "Header", "Hello 1" } }));
_components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(HeaderComponent), new Dictionary<string, object>() { { "Header", "Hello 2" } }));
_components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(HeaderComponent), new Dictionary<string, object>() { { "Header", "Hello 3" } }));
_components.Add(new Tuple<Type, Dictionary<string, object>>(typeof(Counter), new Dictionary<string, object>()));
}
private RenderFragment MyComponents => builder =>
{
foreach (var component in _components)
{
builder.OpenComponent(0, component.Item1);
foreach (var parameter in component.Item2)
{
builder.AddAttribute(1, parameter.Key, parameter.Value);
}
builder.CloseComponent();
}
};
}
I've used a Tuple to keep things simple: you could have an object to hold your component information if you want to get a bit more complex.

Related

create a reference to an string (Blazor)

I'm doing a Blazor app, although I have the impression the question is C# and independent to blazor.
I have a class with some strings (like Person.Name, Person.Surname, etc)
I have a person viewer component that shows those strings. And a "string editor" component so when the user clicks on the name of the patient, the string editor component will appear and allow you to edit the name (It is not exactly like that, but will help us simplify the scenario and find a solution)
The question is how to send a reference to Person.Name to the StringEditor component, so the original string is also edited when it is edited there. If I was an object is simple, you send the object and just a reference is sent so changes done in the component apply to the original source object but for strings... this does not work like this. A copy is sent, and the changes are not carried over to the original component.
The scenario would look a bit like :
Person class
public class Person{
public string Name {get;set;} = "";
public string Surname {get;set;} = "";
}
Main razor component
<div>
<div #onclick=()=>{OpenStringEditor(user.Name)}>#user.Name</div>
<div #onclick=()=>{OpenStringEditor(user.Surname)}>#user.Surname</div>
</div>
<div class=#(ShowEditor ? "ShowEditor" : "HideEditor")>
<StringEditor StringToEdit EditFinished=whenFinished/>
</div>
code{
[Parameter] pubic Person user {get; set;}
private void OpenStringEditor(string ToEdit)
{
StringToEdit = ToEdit; // This is where I'd need to keep a reference
// to the original source, so when the
// edit is finished I can store the end product
ShowEditor = true;
}
private void whenFinished()
{
Person.StoreToDB(); //whatever
ShowEditor = false;
}
}
StringEditor
<input #bind=ToBeEdited #onfocusout=EditFinished />
code
{
[Parameter] public string ToBeEdited;
[Parameter] public EventCallback EditFinished;
}
So you see, this would work for objects, but not for raw strings
I guess the bottom question would be: how to keep a reference of "where to store the changes when the edit is finished."
Do this fit the bill?
First you need a Modal Dialog Framework. I've included a simple one - the code is in appendix to this answer, but you can use any of those available that are suitable.
A "Text Editor" component which we will use in the modal dialog. If you're editing other data types you will need different ones. It interacts with the cascaded instance of the Modal Dialog wrapper.
TextEditor.razor
<div class="mb-3">
<label class="form-label small text-muted">#this.Label</label>
<div class="input-group">
<input class="form-control" value="#this.Value" #oninput=this.Changed />
<button class="btn btn-success" #onclick=Exit>Set</button>
<button class="btn btn-danger" #onclick=Cancel>Cancel</button>
</div>
</div>
#code {
[CascadingParameter] private IModalDialog? Modal { get; set; }
[Parameter] public string Label { get; set; } = "Field";
[Parameter] public string? Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>>? ValueExpression { get; set; }
private string? _value;
protected override void OnInitialized()
=> _value = Value;
private void Changed(ChangeEventArgs e)
=> this.ValueChanged.InvokeAsync(e.Value?.ToString() ?? null);
private void Exit()
=> Modal?.Close(ModalResult.OK());
private async Task Cancel()
{
await this.ValueChanged.InvokeAsync(_value);
Modal?.Close(ModalResult.Cancel());
}
}
And then our "Special Format" page:
#page "/"
<PageTitle>Index</PageTitle>
<h1 class="mb-5">Hello, world!</h1>
<div class="mt-5 mb-5" >
Editor
</div>
<div class="mt-5 border border-1 border-dark p-1" style="cursor:pointer;" #onclick="() => Edit(CountryModalOptions)">
#model.Country
</div>
<div class="mt-2 border border-1 border-dark p-1" style="cursor:pointer;" #onclick="() => Edit(ContinentModalOptions)">
#model.Continent
</div>
<BaseModalDialog #ref=this.Modal />
#code {
private IModalDialog? Modal;
private Model model = new();
private async Task Edit(ModalOptions options)
{
if (Modal is not null)
{
var result = await Modal.ShowAsync<TextEditor>(options);
// do things
}
}
private ModalOptions CountryModalOptions
{
get
{
var options = new ModalOptions();
options.ControlParameters.Add("Label", "Field Name");
options.ControlParameters.Add("Value", model.Country);
options.ControlParameters.Add("ValueChanged", EventCallback.Factory.Create<String>(this, RuntimeHelpers.CreateInferredEventCallback(this, __value => model.Country = __value, model.Country)));
options.ControlParameters.Add("ValueExpression", RuntimeHelpers.TypeCheck<System.Linq.Expressions.Expression<System.Func<System.String>>>(() => model.Country));
return options;
}
}
private ModalOptions ContinentModalOptions
{
get
{
var options = new ModalOptions();
options.ControlParameters.Add("Label", "Field Name");
options.ControlParameters.Add("Value", model.Continent);
options.ControlParameters.Add("ValueChanged", EventCallback.Factory.Create<String>(this, RuntimeHelpers.CreateInferredEventCallback(this, __value => model.Continent = __value, model.Continent)));
options.ControlParameters.Add("ValueExpression", RuntimeHelpers.TypeCheck<System.Linq.Expressions.Expression<System.Func<System.String>>>(() => model.Continent));
return options;
}
}
public class Model
{
public string Country { get; set; } = "No Country Set";
public string Continent { get; set; } = "No Continent Set";
}
}
So what's going on.
We have our BaseModalDialog component registered on the page with a global variable reference. When you click on an editable field the onclick handler opens the modal dialog by calling ShowAsync. It sets the component as T and passes in a set of Parameters to apply to T when it's defined. The standard bind Parameters are mapped to the model field using some RuntimeHelpers.
The dialog opens and you can edit the value. I've wired it up to update on each keypress (oninput). Click on either Set or Cancel to exit the model as pass "control" back to the main page.
I've kept everything as basic as I can, there are all sorts of Css and formating options you can apply to improve the UX.
Appendix The Modal Dialog
ModalOptions
public sealed class ModalOptions : IEnumerable<KeyValuePair<string, object>>
{
public string Width { get; set; } = "50%";
public Dictionary<string, object> ControlParameters { get; } = new Dictionary<string, object>();
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
foreach (var item in ControlParameters)
yield return item;
}
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
public T? Get<T>(string key)
{
if (this.ControlParameters.ContainsKey(key))
{
if (this.ControlParameters[key] is T t) return t;
}
return default;
}
public bool TryGet<T>(string key, [NotNullWhen(true)] out T? value)
{
value = default;
if (this.ControlParameters.ContainsKey(key))
{
if (this.ControlParameters[key] is T t)
{
value = t;
return true;
}
}
return false;
}
public bool Set(string key, object value)
{
if (this.ControlParameters.ContainsKey(key))
{
this.ControlParameters[key] = value;
return false;
}
this.ControlParameters.Add(key, value);
return true;
}
}
ModalResult
public enum ModalResultType { NoSet, OK, Cancel, Exit }
public sealed class ModalResult
{
public ModalResultType ResultType { get; private set; } = ModalResultType.NoSet;
public object? Data { get; set; } = null;
public static ModalResult OK() => new ModalResult() { ResultType = ModalResultType.OK };
public static ModalResult Exit() => new ModalResult() { ResultType = ModalResultType.Exit };
public static ModalResult Cancel() => new ModalResult() { ResultType = ModalResultType.Cancel };
public static ModalResult OK(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.OK };
public static ModalResult Exit(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.Exit };
public static ModalResult Cancel(object data) => new ModalResult() { Data = data, ResultType = ModalResultType.Cancel };
}
IModalDialog
public interface IModalDialog
{
public ModalOptions Options { get; }
public bool IsActive { get; }
public bool Display { get; }
public Task<ModalResult> ShowAsync<TModal>(ModalOptions options) where TModal : IComponent;
public void Dismiss();
public void Close(ModalResult result);
public void Update(ModalOptions? options = null);
}
BaseModalDialog.razor
#namespace Blazr.UI
#implements IModalDialog
#if (this.Display)
{
<CascadingValue Value="(IModalDialog)this">
<div class="base-modal-background">
<div class="base-modal-content" style="width:#this.Options.Width;" #onclick:stopPropagation="true">
<DynamicComponent Type=this.ModalContentType Parameters=this.Options.ControlParameters />
</div>
</div>
</CascadingValue>
}
#code {
public ModalOptions Options { get; protected set; } = new ModalOptions();
public bool Display { get; protected set; }
public bool IsActive => this.ModalContentType is not null;
protected TaskCompletionSource<ModalResult> _ModalTask { get; set; } = new TaskCompletionSource<ModalResult>();
protected Type? ModalContentType = null;
public Task<ModalResult> ShowAsync<TModal>(ModalOptions options) where TModal : IComponent
{
this.ModalContentType = typeof(TModal);
this.Options = options ??= this.Options;
this._ModalTask = new TaskCompletionSource<ModalResult>();
this.Display = true;
InvokeAsync(StateHasChanged);
return this._ModalTask.Task;
}
public void Update(ModalOptions? options = null)
{
this.Options = options ??= this.Options;
InvokeAsync(StateHasChanged);
}
public async void Dismiss()
{
this._ModalTask.TrySetResult(ModalResult.Cancel());
await Reset();
}
public async void Close(ModalResult result)
{
this._ModalTask.TrySetResult(result);
await Reset();
}
private async Task Reset()
{
this.Display = false;
this.ModalContentType = null;
await InvokeAsync(StateHasChanged);
}
}
BaseModalDialog.razor.css
div.base-modal-background {
display: block;
position: fixed;
z-index: 101; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
div.base-modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 10px;
border: 2px solid #888;
width: 50%;
}

Blazor [Parameter] not updating input variables

I have a custom component InlineAutocomplete. No matter what I do my pic value and SelectedPIC value are always null when I do validation and unsure why it is.
The component does render correctly, but can't get my parameter back with the values set correctly.
Usage as follow:
<InlineAutocomplete TItem="PIC"
Value="#pic"
Selected="#SelectedPIC"
Data="#PICs"
Property="#((item) => item.PICNumber)" />
My component:
<div class="inline-autocomplete">
<input #oninput="(e)=> SuggestItem(e)" type="text" #bind-value="#Value" class="overlap inline-autocomplete-input " />
<input readonly class="overlap inline-autocomplete-label" value="#_suggestedValue" />
</div>
#code {
string _value;
string _suggestedValue;
[Parameter]
public string Value
{
get => _value;
set
{
if (value == null)
return;
_value = value;
}
}
[Parameter]
public List<TItem> Data { get; set; }
[Parameter]
public Func<TItem, string> Property { get; set; }
[Parameter]
public TItem Selected { get; set; }
void SuggestItem(ChangeEventArgs e)
{
if (string.IsNullOrEmpty(e.Value.ToString()))
return;
Value = e.Value.ToString().ToUpper();
if (string.IsNullOrEmpty(Value))
{
_suggestedValue = string.Empty;
}
else
{
var selectedItem = Data.Select(Property).FirstOrDefault(x => x.StartsWith(Value, StringComparison.OrdinalIgnoreCase));
_suggestedValue = !string.IsNullOrWhiteSpace(selectedItem) ? selectedItem : string.Empty;
Selected = Data.FirstOrDefault(x => string.Equals(Property(x), Value, StringComparison.OrdinalIgnoreCase));
StateHasChanged();
}
// State has changed
StateHasChanged();
}
}
The solution to this was to pass down a function that changes the property in the Parent instead of the Child.
Child:
[Parameter]
public Action<string, TItem> ValueChanged { get; set; }
...
#code {
void SomeFunction() {
ValueChanged(Value, Selected);
}
}
Parent:
<InlineAutocomplete ... ValueChanged="FireEvent" />
void FireEvent(string stringVal, MyClass c)
{
// ... Do Something
}

How to get return value from EventCallback in blazor?

My situation is this: I'm trying to implement and Autocomplete.
The Autocomplete will have a Parameter that will receive a string and return a IEnumerable<TValue>.
Here is an example of what I'm trying to do
Autocomplete.razor
#code {
[Parameter]
public SOME_TYPE GetItems { get; set; }
async void Foo(){
IEnumerable<TValue> items = await GetItems(SomeString);
// do something with items
}
}
ParentComponent.razor
<Autocomplete TValue="SomeEntity"
GetItems="#GetItems" />
#code {
SOME_TYPE GetItems(string name) {
IEnumerable<SomeEntity> entity = await GetEntitys(name);
return entity;
}
}
The problem is that I don't know what to put in SOME_TYPE. Should I use EventCallback? Action? What should I use?
I tried using EventCallback but looks like I can't get a return value from EventCallback? I have no idea.
I just find out how to do it, I should use Func<string, Task<IEnumerable<TValue>>>.
[Parameter]
public Func<string, Task<IEnumerable<TValue>>> GetItems { get; set; }
And
public async Task<IEnumerable<Employee>> GetItems(string name) {
IEnumerable<SomeEntity> entity = await GetEntitys(name);
return entity;
}
You can also use argument of EventCallback for that:
public class CancelArg
{
public bool Value { get; set; }
}
[Parameter]
public EventCallback<CancelArg> OnFoo { get; set; }
async Task CallerInChild()
{
var cancel = new CancelArg();
await OnFoo.InvokeAsync(cancel);
if (cancel.Value)
{
// cancelled ...
}
}
void HandlerInParent(CancelArg cancel)
{
cancel.Value = true;
}
In a child component:
<div class="form-group">
<label for="ddSetStatus">Status</label>
<InputSelect class="form-control" id="ddSetStatus" #bind-Value="#Status" #oninput="#((e) => ChangedSelection(e))">
<option value="1">Draft </option>
<option value="2">Published </option>
<option value="3">Archived </option>
</InputSelect>
</div>
#code
{
[Parameter] public EventCallback<int> OnStatusSelected { get; set; }
[Parameter] public int Status { get; set; }
private async Task ChangedSelection(ChangeEventArgs args)
{
await OnStatusSelected.InvokeAsync(int.Parse(args.Value.ToString()));
}
}
and then consuming the selected value in a parent page or component:
create a component (called DocStatus in this example) and define the event handling method
in markup area:
<DocStatus Status="#Status" OnStatusSelected="StatusSelectionHandler"/>
in the code area:
private void StatusSelectionHandler(int newValue)
{
Status = newValue;
}

Dynamically binding input-text to class/object propertys using Blazor

Im trying to build a dynamic list of input field for properties inside a class using Blazor but cant figur out how to bind/link the content of a input box to a property of a class. (the class have can have a large number of public props, not only Name and Description as in the below example, they are not always of the type "string")
lets say that I have this class/model:
public class customer{
public string Name { get; set; }
public int Age { get; set; }
public string Description { get; set; }
}
I got this blazor component (updateC.razor):
#inherits CLogic
#if (nfo != null)
{
#foreach (var obj in nfo)
{
<input type="text" class="form-control"
bind=#SCustomer.GetType().GetProperty(obj.ToString())/>
}
}
and finally Clogic:
public class Clogic: ComponentBase{
[Parameter]
public Customer SCustomer { get; set; } = new Customer();
[Parameter]
public PropertyInfo[] nfo { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
nfo = SCustomer.GetType().GetProperties();
StateHasChanged();
}
}
}
This is suppose to bind changes made in each input field to the correct property in the current instance of SCustomer (when input is made it is suppose to update the correct property of the class/object). This is not working, values inside of SCustomer are not changed after input are done. I'm guessing that i'm going about this completely wrong but can't seem to figure out how to make this work and can't find any examples doing this.
#foreach (var propertyInfo in nfo)
{
<input type="text" class="form-control"
value="#propertyInfo.GetValue(SCustomer)"
#onchange="#((ChangeEventArgs __e) =>
propertyInfo.SetValue(SCustomer, __e.Value.ToString()))" />
}
#foreach(propertyInfo in nfo)
{
<label>#propertyInfo.Name</label>
<input type="text" value="#propertyInfo.GetValue(SCustomer)" #onchange="#((ChangeEventArgs __e) =>
propertyInfo.SetValue(SCustomer,Convert.ChangeType(__e.Value,
propertyInfo.PropertyType,null))"/>
}
I finally found a way of doing this, here is my solution:
I created a helper class:
public class PropHolder
{
[Parameter]
public PropertyInfo info { get; set; }
[Parameter]
public string type { get; set; }
public PropHolder(PropertyInfo nfo, string propType)
{
info = nfo;
type = propType;
}
}
then i created a dictionary of this class and some cheking functions (this is inside of Clogic)
[Parameter]
public Dictionary<int, PropHolder> Props{ get; set; }
public void GetAllProps()
{
Props = new Dictionary<int, PropHolder>();
//nfo = SCustomer.GetType().GetProperties();
int Cid = 0;
foreach (PropertyInfo pif in SCustomer.GetType().GetProperties())
{
Props[Cid] = new PropHolder(pif, pif.PropertyType.Name);
Cid++;
}
}
public string CheckName(PropHolder propertyInfo)
{
if (propertyInfo.GetType() == typeof(PropHolder))
{
return propertyInfo.type;
}
else
{
return propertyInfo.GetType().Name.ToString();
}
}
public PropertyInfo getInfo(PropHolder propertyInfo)
{
if (propertyInfo.GetType() == typeof(PropHolder))
{
return propertyInfo.info;
}
else
{
return null;
}
}
and finally im able to loop over the keys of my dictionary and bind all values correctly (got lots of help figuring this out from the answere given by: "agua from mars")
here is the updateC.razor content:
#if (Props != null)
{
#foreach (int key in Props.Keys)
{
var pinfo = Props[key];
#if (CheckName(pinfo) == "String")
{
<input type="text" class="form-control" value=#(getInfo(pinfo).GetValue(SCustomer)) #onchange="#((ChangeEventArgs __e) => getInfo(pinfo).SetValue(SCustomer, __e.Value.ToString()))" />
}
}
}
this gives me a input box for every prop that is of the type String, if additional types are to be handled it can now easily be added. This is probably not the best sulution but it do work. I will update the answere if any better working solutions are posted.
I solved this using generics. This lets each input type pass on a generic type which defines what type it wants to use.
<input #bind="Name.Value" />
<input #bind="Age.Value" />
<div><code>Name</code>: #Name.Value</div>
<div><code>Age</code>: #Age.Value</div>
#code {
private FormField<string> Name { get; set; } = new FormField<string>();
private FormField<int> Age { get; set; } = new FormField<int>();
public class Form
{
public ICollection<IFormField> Fields { get; set; }
}
public interface IFormField
{
public int ControlType { get; set; }
}
public class FormField<T> : IFormField
{
public int ControlType { get; set; }
public T Value { get; set; }
}
}
Here it is on BlazorFiddle: https://blazorfiddle.com/s/wen1g26q

Call method in component

I have this "Alert" component:
#if (Show)
{
<div class="alert #Class" role="alert">
#Text
</div>
}
#functions
{
[Parameter]
private bool Show { get; set; } = false;
[Parameter]
private string Text { get; set; } = String.Empty;
[Parameter]
private string Class { get; set; } = String.Empty; //Success, Warning etc.
}
However, when I call this component on my page I still need to create atleast two variables - ShowError and ErrorText - to handle the state of this alert still clutters my code a lot as this alert exists on practically all pages.
My question is: Is it possible to de-clutter the code by calling a ShowMessage method in the child component?
An example would be something like this:
Page
#page "/my-page"
#inject HttpClient Http
<!-- A lot of HTML code here -->
<Alert/>
<!-- A lot of HTML code here -->
#functions {
protected override async Task OnInitAsync()
{
var response = await Http.PostJsonAsync<Response>("/api/sessions/create", null);
if (response.StatusCode == HttpStatusCode.OK)
{
}
else
{
myAlertComponent.ShowSuccessMessage(response.Message);
}
}
}
"Alert" component
#if (Show)
{
<div class="alert #Class" role="alert">
#Text
</div>
}
#functions
{
[Parameter]
private bool Show { get; set; } = false;
[Parameter]
private string Text { get; set; } = String.Empty;
[Parameter]
private string Class { get; set; } = String.Empty; //Success, Warning, Danger
public void HideAlerts()
{
Show = false;
}
public void ShowSuccessMessage(string message)
{
Show = true;
Text = message;
Class = "success":
}
public void ShowErrorMessage(string message)
{
Show = true;
Text = message;
Class = "danger":
}
}
To call component methods try adding a reference to the component with #ref and then adding the component declaration in the #code block. If the methods are public in the component you can use them outside of the component's scope.
Parent.razor
<Alert #ref="Alert" Text="I am showing" /> #*Notice the #ref tag;*#
<button #onclick="() => ShowAlert(true)">Show Success</button>
<button #onclick="() => ShowAlert(false)">Show Failure</button>
<button #onclick="HideAlert">Hide</button>
#code {
private Alert Alert { get; set; } // This is where the #ref is bound; no need to initialize, the markup does that for you.
private void ShowAlert(bool success)
{
if (success) Alert.ShowSuccessMessage("Success!!");
else Alert.ShowErrorMessage("Failed :(");
}
private void HideAlert() => Alert.Hide();
}
Alert.razor
#if (_show)
{
<div class="alert alert-#_class" role="alert">
<strong>#Text</strong>
</div>
}
#code
{
[Parameter] public string Text { get; set; } = string.Empty; // set as "I am showing", but will be updated if a message is passed to the show methods below
private bool _show { get; set; }
private string _class { get; set; } = string.Empty;
public void Hide() { _show = false; StateHasChanged(); }
public void ShowSuccessMessage(string message? = null)
{
_show = true;
Text = message ?? Text ?? string.Empty;
_class = "success";
StateHasChanged(); // StateHasChanged() informs the parent component that the child has updated and to update the dom
}
public void ShowErrorMessage(string? message = null)
{
_show = true;
Text = message ?? Text ?? string.Empty;
_class = "danger";
StateHasChanged();
}
}
See Section: Capture references to components

Categories