skip to Main Content

I am trying to switch from CSS to Tailwind, a CSS framework that generates the CSS for you by typing values into the class attribute of an HTML element. One of the major problems is that when you have multiple elements with the same CSS, you end up with long and redundant classes that can be outsourced. This is a simple example:

RapportGrid.razor

<div class="grid auto-cols-fr grid-rows-auto-rows gap-y-1 gap-x-8 self-stretch my-4 border-md shadow-shadowgreybig py-[7px] px-4 @variantClass @AdditionalClasses">
    @ChildContent
</div>

@code {
    [Parameter] public RapportGridVariant Variant { get; set; }

    [Parameter] public RenderFragment ChildContent { get; set; }
    [Parameter] public string AdditionalClasses { get; set; }

    private string variantClass
    {
        get
        {
            switch (Variant)
            {
                case RapportGridVariant.General: return "!bg-whitecolor-300";
                case RapportGridVariant.Time: return "!bg-bluecolor-300";
                case RapportGridVariant.Goodwill: return "!bg-redcolor-200";
                case RapportGridVariant.Expenses: return "!bg-redcolor-100";
                case RapportGridVariant.Booking: return "!bg-greencolor-100";
                case RapportGridVariant.Sign: return "!bg-bluecolor-100";
                case RapportGridVariant.Article: return "!bg-bluecolor-200";
                default: return "";
            }
        }
    }
}

Use case:

<RapportGrid Variant="RapportGridVariant.Time">
<!-- Content -->
</RapportGrid>

As you can see, this is essentially a div that wraps the content with a RenderFragment. Now I have an where the CSS has to be directly injected into the class-attribute of this element:

<input class="input-text nomargin centerInputContentVertical"
       type="time"
       id="startTime"
       @ref="startTimeElement"
       @bind-value="blazorStartTime"
       @onblur="HandleStartTimeSet">

startTimeElement: for focusing the element
blazorStartTime: Type:DateTime, Validating and Triggering Events
HandleStartTimeSet: Calculating TimeSpan and setting values in other ViewModels

How can I create a variable razor-component where you can set these three values as parameters and the input is moved to the component?

I already created a component named "InputTextField.zaor" that looks like this:

<input class="transition transition-shadow duration-[40ms] ease-out border border-solid border-whitecolor-500 rounded-md bg-whitecolor-200 text-whitecolor-900 focus-visible:border-main-500 focus-visible:shadow-shadowblue @(NoMargin ? "!my-0" : "" ) @(Licence ? "!rounded-lg !py-[26px] !font-Inconsolata !text-2xl !leading-none !font-bold" : "") @(CenterInputContentVertical ? "!align-middle !flex !justify-center !itmes-center" : "") @AdditionalClasses"
       type="time"
       id="startTime">


@code {
    [Parameter] public string AdditionalClasses { get; set; }
    [Parameter] public bool NoMargin { get; set; }
    [Parameter] public bool Licence { get; set; }
    [Parameter] public bool CenterInputContentVertical { get; set; }
}

I also tried attribute splatting, inserting attributes via a dictionary & passing the values via Parameters.

Nothing worked as intended for me and I’ve already invested way too many hours to solve this. I hope someone can help me out with my issue 🙂

2

Answers


  1. Chosen as BEST ANSWER

    I've done some research and came to this solution. I think you can still make some code-cleanup and simplifiy the component. But at this state I'm pretty satisfied with my solution:

    InputField.razor

    <input class="transition transition-shadow duration-[40ms] ease-out border border-solid border-whitecolor-500 rounded-md bg-whitecolor-200 text-whitecolor-900 focus-visible:border-main-500 focus-visible:shadow-shadowblue @(NoMargin ? "!my-0" : "" ) @(Licence ? "!rounded-lg !py-[26px] !font-Inconsolata !text-2xl !leading-none !font-bold" : "") @(CenterInputContentVertical ? "!align-middle !flex !justify-center !itmes-center" : "") @AdditionalClasses"
           type="@variantClass"
           id="startTime"
           value="@FormattedValue"
           @onchange="OnValueChanged"
           @onblur="OnValueBlur"
           @oninput="OnValueInput"
           @ref="@InputRef">
    
    @code {
        [Parameter] public string AdditionalClasses { get; set; }
        [Parameter] public bool NoMargin { get; set; }
        [Parameter] public bool Licence { get; set; }
        [Parameter] public bool CenterInputContentVertical { get; set; }
    
        [Parameter] public TextFieldVariant Variant { get; set; }
        [Parameter] public ElementReference InputRef { get; set; }
        [Parameter] public EventCallback Reaction { get; set; }
    
        [Parameter] public DateTime TimeValue { get; set; }
        [Parameter] public EventCallback<DateTime> TimeValueChanged { get; set; }
    
        [Parameter] public double NumberValue { get; set; }
        [Parameter] public EventCallback<double> NumberValueChanged { get; set; }
    
        private string variantClass
        {
            get
            {
                switch (Variant)
                {
                    case TextFieldVariant.Time: return "time";
                    case TextFieldVariant.Date: return "date";
                    case TextFieldVariant.Number: return "number";
                    default: return "";
                }
            }
        }
    
        private string FormattedValue
        {
            get
            {
                switch (Variant)
                {
                    case TextFieldVariant.Time: return TimeValue.ToString("HH:mm:ss");;
                    case TextFieldVariant.Date: return "date";
                    case TextFieldVariant.Number: return NumberValue.ToString();
                    default: return "";
                }
            }
        }
    
        async Task OnValueChanged(ChangeEventArgs args)
        {
            if (Variant == TextFieldVariant.Time && DateTime.TryParse(args.Value.ToString(), out DateTime timeResult))
            {
                TimeValue = timeResult;
                await TimeValueChanged.InvokeAsync(TimeValue);
            }
        }
    
        async Task OnValueInput(ChangeEventArgs args)
        {
            if (Variant == TextFieldVariant.Number && double.TryParse(args.Value.ToString(), out double numberResult))
            {
                NumberValue = numberResult;
                await NumberValueChanged.InvokeAsync(NumberValue);
            }
        }
    
        async Task OnValueBlur()
        {
            await Reaction.InvokeAsync();
        }
    }
    

    Sample Use-Case

    <InputField Variant="TextFieldVariant.Time"
                NoMargin="true"
                CenterInputContentVertical="true"
                InputRef="@startTimeElement"
                Reaction="HandleTimeSet"
                @bind-TimeValue="blazorStartTime" />
    

    I know that there are multiple uncertainties why I did it in this way. To clarify that, I need to explain every single purpose of each variable which will go beyond the scope^^


  2. How can I create a variable razor-component where you can set these three values as parameters and the input is moved to the component?

    Update

    On clarifcation in the comments here’s a version of your component with the binding set up and the ref. Note that this is set up for the TimeOnly type as that is the Input type. You may need to adjust this. I haven’t complicated the control by setting up different typing as you have commented about your experience level in the comment. I’ve also added code to help set up first focus as that’s possibly why you are using element.

    @using System.Linq.Expressions
    @using System.Diagnostics.CodeAnalysis
    @using System.Globalization;
    
    <input class="transition transition-shadow duration-[40ms] ease-out border border-solid border-whitecolor-500 rounded-md bg-whitecolor-200 text-whitecolor-900 focus-visible:border-main-500 focus-visible:shadow-shadowblue @(NoMargin ? "!my-0" : "" ) @(Licence ? "!rounded-lg !py-[26px] !font-Inconsolata !text-2xl !leading-none !font-bold" : "") @(CenterInputContentVertical ? "!align-middle !flex !justify-center !itmes-center" : "") @AdditionalClasses"
           type="time"
           id="startTime"
           value="@this.Value"
           @onchange=this.OnValueChanged
           @ref=this.Element>
    
    
    @code {
        [Parameter] public string AdditionalClasses { get; set; } = string.Empty;
        [Parameter] public bool NoMargin { get; set; }
        [Parameter] public bool Licence { get; set; }
        [Parameter] public bool CenterInputContentVertical { get; set; }
        [Parameter] public bool InitialFocus { get; set; }
    
        [Parameter] public TimeOnly? Value { get; set; }
        [Parameter] public EventCallback<TimeOnly?> ValueChanged { get; set; }
    
        [DisallowNull] public ElementReference? Element { get; protected set; }
    
        private void OnValueChanged(ChangeEventArgs e)
        {
            if (BindConverter.TryConvertToTimeOnly(e.Value?.ToString(), CultureInfo.InvariantCulture, out TimeOnly timeValue))
                this.ValueChanged.InvokeAsync(timeValue);
        }
    
        protected async override Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender  && this.InitialFocus && this.Element.HasValue)
                await this.Element.Value.FocusAsync();
        }
    }
    

    And demo code:

    <div class="m-2">
        <InputTextField @bind-Value=this.data InitialFocus=true />
    </div>
    
    
    <div class="alert alert-info">
        @this.data.ToString()
    </div>
    @code {
        private TimeOnly? data;
    }
    

    This is the original answer

    I’m not quite sure which three parameters, so if I’m wrong in my assumptions of your intent please tell me. I’m happy to clarify anything. 🙂

    On the Css you can create a CSSBuilder class [at the bottom of this answer] that constructs the Css in a more structured way. For example:

    <div class="@this.MyCss.Build()">XXXX</div>
    
    @code {
        [Parameter] public string? AdditionalClasses { get; set; }
        [Parameter] public bool NoMargin { get; set; }
    
        private CSSBuilder MyCss => new CSSBuilder("transition transition-shadow duration-[40ms] ease-out")
                .AddClass(this.NoMargin, "my-0")
                .AddClass(this.NoMargin, "xxx", "yyy")
                .AddClass(this.AdditionalClasses);
    }
    

    You’ll find several different implementations of this if your search around. you can extent it’s functionality to fit your specific needs.

    On building complex input type components, there’s a recent question and answer here that demonstrates some different approaches.

    How do I pass a @bind-Value to a child component?.

    public sealed class CSSBuilder
    {
        private Queue<string> _cssQueue = new Queue<string>();
    
        public static CSSBuilder Class(string? cssFragment = null)
            => new CSSBuilder(cssFragment);
    
        public CSSBuilder() { }
    
        public CSSBuilder(string? cssFragment)
            => AddClass(cssFragment ?? String.Empty);
    
        public CSSBuilder AddClass(string? cssFragment)
        {
            if (!string.IsNullOrWhiteSpace(cssFragment))
                _cssQueue.Enqueue(cssFragment);
            return this;
        }
    
        public CSSBuilder AddClass(IEnumerable<string> cssFragments)
        {
            cssFragments.ToList().ForEach(item => _cssQueue.Enqueue(item));
            return this;
        }
    
        public CSSBuilder AddClass(bool WhenTrue, string cssFragment)
            => WhenTrue ? this.AddClass(cssFragment) : this;
    
        public CSSBuilder AddClass(bool WhenTrue, string? trueCssFragment, string? falseCssFragment)
            => WhenTrue ? this.AddClass(trueCssFragment) : this.AddClass(falseCssFragment);
    
        public CSSBuilder AddClassFromAttributes(IReadOnlyDictionary<string, object> additionalAttributes)
        {
            if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
                _cssQueue.Enqueue(val.ToString() ?? string.Empty);
            return this;
        }
    
        public CSSBuilder AddClassFromAttributes(IDictionary<string, object> additionalAttributes)
        {
            if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
                _cssQueue.Enqueue(val.ToString() ?? string.Empty);
            return this;
        }
    
        public string Build(string? CssFragment = null)
        {
            if (!string.IsNullOrWhiteSpace(CssFragment)) _cssQueue.Enqueue(CssFragment);
            if (_cssQueue.Count == 0)
                return string.Empty;
            var sb = new StringBuilder();
            foreach (var str in _cssQueue)
            {
                if (!string.IsNullOrWhiteSpace(str)) sb.Append($" {str}");
            }
            return sb.ToString().Trim();
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search