Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/EdCharbeneau/BlazorComponentUtilities

CssBuilder is a Builder pattern for CSS classes to be used with Razor Components.
https://github.com/EdCharbeneau/BlazorComponentUtilities

Last synced: 19 days ago
JSON representation

CssBuilder is a Builder pattern for CSS classes to be used with Razor Components.

Awesome Lists containing this project

README

        

# CSS & Style Builder Pattern

**[Read the full API docs in the Wiki section][docs]**

When building complex components for ASP.NET Razor Components there are often CSS classes generated by multiple factors.
Styles can often be added by system & component state, public parameters, and static base CSS classes.
Combining many CSS classes together doesn't scale well, and when components get large it can be tough to reason about.
The CSS builder pattern fixes this problem by offering a easy to repeat, easy to read pattern to use across components in a project.
A clean code approach to conditional CSS in Razor Components.

## The problem

Given the component below, the Tab's CSS class is set by multiple sources.
The component has paramater `UserCss` which can be set at externally.
A base CSS class `bl-nav-link` which does not change.
Two state based CSS classes `bl-active` and `bl-disabled` which are dependent on if the Tab is active or disabled.
Rendering for the CSS class is spread across multiple concerns and is hard to maintain.

**Component**
```html
...
```

**Component Source Markup**
```html



  • @Title


  • ```
    **Component Source Logic**
    ```csharp
    [Parameter] string UserCss = String.Empty;
    protected string ActiveCssClass => IsActive ? "bl-active" : String.Empty;
    protected string DisabledCssClass => Disabled ? "bl-disabled" : String.Empty;
    ```

    ## The Solution

    Using the CSS Builder pattern we can clean up the code and move everything to a single concern.
    Refactoring the component, we move all of the CSS code to the components logic.
    In the `OnParameterSet` lifecycle method, we condense all of the CSS responsibilities into a single variable `ClassToRender` (this name variable is optional).
    Next, using the `CssBuilder` we define what classes should be added to the builder and when they should be rendered.
    The pattern is as follows `AddClass` the CSS class and the condition when it should appear `AddClass(value, when: bool)`.
    If the value is static, we can discard the `when` parameter.
    Finally, Build is called. All CSS classes are then combined into a single string, properly spaced and trimmed.

    The refactored code becomes:

    **Component Source Markup (refactor)**
    ```html



  • @Title


  • ```

    **Component Source Logic (refactor)**
    ```csharp
    protected override void OnParametersSet()
    {
    ClassToRender = new CssBuilder(UserCss)
    .AddClass("bl-nav-link")
    .AddClass("bl-active", when: IsActive)
    .AddClass("bl-disabled", when: Disabled)
    .Build();
    }
    ```

    **Component Source Logic (refactor with [SetPrefix](https://github.com/EdCharbeneau/CssBuilder/wiki/CssBuilder.SetPrefix-Method))**
    ```csharp
    protected override void OnParametersSet()
    {
    ClassToRender = new CssBuilder(UserCss)
    .SetPrefix("bl-")
    .AddClass("nav-link")
    .AddClass("active", when: IsActive)
    .AddClass("disabled", when: Disabled)
    .Build();
    }
    ```

    ## Dynamic values

    If you encounter a dynamic value where some string is added to a CSS class name, we can easily handle this as well.
    Consider the CSS namespace `bl-` must be prefixed to a value `SomeValue` supplied by the component state or user input.
    This is easily handled with string interpolation `${var}`.

    ```csharp
    protected override void OnParametersSet()
    {
    ClassToRender = new CssBuilder(UserCss)
    .AddClass("bl-nav-link")
    .AddClass("bl-active", when: IsActive)
    .AddClass("bl-disabled", when: Disabled)
    .AddClass("bl-${SomeValue}", when: IsConditionMet)
    .Build();
    }
    ```
    ## Attribute Splatting

    If you encounter dynamic values due to [attribute splatting](https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1#attribute-splatting-and-arbitrary-parameters), we can easily handle this as well.
    Consider the senario where we need to merge a splatted CSS attribute while preserving default values.

    ```html


    ```

    ```csharp

    @code {

    [Parameter(CaptureUnmatchedValues = true)]
    public IReadOnlyDictionary AdditionalAttributes { get; set; } = new Dictionary();

    CssBuilder CssClass => new CssBuilder("my-base").AddClassFromAttributes(attributes);
    }
    ```

    ## Func When

    Func is also accepted as an arguement for the `when` parameter. This allows either inline functions or named functions to be called directly.

    ```csharp
    protected override void OnParametersSet()
    {
    ClassToRender = new CssBuilder(UserCss)
    .AddClass("bl-nav-link")
    .AddClass("bl-foo", when: ()=>
    !string.IsNullOrEmpty(Foo) ||
    !Disabled ||
    IsActive)
    .AddClass("bl-active", when: IsActive)
    .AddClass("bl-disabled", when: Disabled)
    .Build();
    }
    ```

    Named function, example.

    ```csharp

    bool HasMeaningfulName() => !string.IsNullOrEmpty(Foo) || !Disabled || IsActive);

    protected override void OnParametersSet()
    {
    ClassToRender = new CssBuilder(UserCss)
    .AddClass("bl-nav-link")
    .AddClass("bl-foo", when: HasMeaningfulName)
    .AddClass("bl-active", when: IsActive)
    .AddClass("bl-disabled", when: Disabled)
    .Build();
    }
    ```
    ## Removing Unused Attributes

    When using dynamic attributes may result in empty attribute. When Blazor renders an attribute that has an empty string value, it will result in an empty attribute tag.
    However, if the value is null the attribute will be excluded. When an empty attribute is expected, the extension method NullIfEmpty can be used to clean up the resulting markup.
    Note: This method is only necessary when no default value is supplied. Ex: new CssBuilder() or CssBuilder().Empty(). Forgetting to call NullIfEmpty should not have any impact on the UI.

    ```html


    ```

    ```csharp

    @code {

    //string CssClass => new CssBuilder().AddClassFromAttributes(attributes).Build();
    string CssClass => new CssBuilder().AddClassFromAttributes(attributes).NullIfEmpty();
    }
    ```
    [docs]: https://github.com/EdCharbeneau/CssBuilder/wiki/Getting-started#getting-setup