Home

Awesome

BlazorStyledTextArea

A textarea with style!

Important

As an early adopter use only for own projects where you are happy to provide feedback and improve this component, resolving any early use issues. Please do not use for company or enterprise applications until this has been shared and used by a good sample of developers.

What it does?

This component essentially remains and works as a textarea but can have any of the text styled based on your application needs. Its simplicity is purposefully designed to avoid the complexities and issues that come with rich text editors.

It provides simple styling rules and efficient handling of line changes, along with inline typeahead features and ability to add your own custom overlayed typeahead selector or other contextual elements.

It does not provide the full support of markup editing and user interaction that rich text editors bring but does provide the styling needs that would be required by most plain text applications.

Demo

Online Demo

Feature Summary

Installation

Latest version in here: NuGet

Install-Package BlazorStyledTextArea

or

dotnet add package BlazorStyledTextArea

The API

<StyledTextArea
    @bind-Value=myText
    @bind-Value:event='oninput'
    StyleRules=myStyleRules
    OnLinesChanged=myOnLinesChangedCallback
    TypeaheadReservedKeys=myTypeaheadReservedKeys
    OnCalculateTypeahead=myOnCalculateTypeaheadCallback
    OnKeyDown=myOnKeyDownCallback
    OnCaretChanged=myOnCaretChangedCallback
    OnElementClicked=MyElementClicked
    TimeElementIsClickableInMilliseconds=1200
    UseStandardTextarea=false>
    <InlineTypeahead>
      style your typeahead text suggestion here
    </InlineTypeahead>
    <CustomTypeahead>
      style your typeahead dropdown selector here
    </CustomTypeahead>
</StyledTextArea>

Parameters

Methods

The StyleRule

StyleRule has a number of composable methods to apply CSS styles to matched text. Each line of text is processed by these rules.

The OnLinesChanged callback event

As the user types, cuts and pastes text any number of lines can change. This callback can be used to process those changed lines and any expired line style rules. This aids efficiencies when parsing large text inputs.

ChangedLines Properties.

The typeahead feature

A common feature as users type is to provide some form of autocomplete, typeahead or dropdown suggestions. The following parameters provide this ability.

Examples

General styling and interaction

This code demonstration shows the following.

Demo

Your razor file.

<style>
    .my-yellow { 
        background-color: yellow; 
    }

    .my-style {
        color: white;
        background-color: green; 
    }

    .keyword {
        background-color: lightcyan;
    }
</style>
<StyledTextArea
    @bind-Value=text 
    @bind-Value:event="oninput"
    StyleRules=styleRules 
    OnElementClicked=MyElementClicked
    UseStandardTextarea=UseStandardTextarea
    spellcheck="false">
</StyledTextArea>
<pre>@interactionMessage </pre>
@code {
    private string text = StyledTextArea.PrepareText("DemoTextStyling.txt".ReadResource());

    private List<StyleRule> styleRules = new List<StyleRule>()
    {
        StyleRule.Words("Styles and templates", "bold").OnLine(0), //only match on line index 0

        //inbuilt styles
        StyleRule.Words("bold", "bold").AtInstance(0), 
        StyleRule.Words("italic", "italic"),
        StyleRule.Words("underline", "underline"),
        StyleRule.Words("strikethrough", "strikethrough"),
        StyleRule.Words("rounded", "rounded my-yellow"),
        StyleRule.Text("*  ", "").ReplaceWith("●  ").AtStartOfLine(),

        //normal style usage
        StyleRule.Words("own styles", "rounded my-style"),
        
        //templates
        StyleRule.Words("github.com", "")
            .HtmlTemplate($"<a href='https://www.github.com' target='_blank'>{StyleRule.MatchMarker}</a>")
            .WithId("link"),

        //nested style where largest match is styled first
        StyleRule.Words("largest match is styled", "bold"),
        StyleRule.Words("match is", "underline"),

        //interactive element, shows message when clicked when not editing.
        StyleRule.Words("(click me)", "rounded my-yellow").WithId("clickable"),
        StyleRule.Words("(or me)", "rounded my-yellow").WithId("clickable"),

        //global keyword styling example
        StyleRule.Words("StyleRule.Text", "rounded keyword"),
        StyleRule.Words("StyleRule.Words", "rounded keyword"),
        StyleRule.Words(".OnLine", "rounded keyword"),
        StyleRule.Words(".HtmlTemplate", "rounded keyword"),
        StyleRule.Words("StyleRule.MatchMarker", "rounded keyword"),
        StyleRule.Words(".WithId", "rounded keyword"),
        StyleRule.Words(".AtInstance", "rounded keyword"),
        StyleRule.Words(".AtStartOfLine", "rounded keyword"),
        StyleRule.Words("OnElementClicked", "rounded keyword")
    };

    private string interactionMessage = "";

    private void MyElementClicked(Element element) => interactionMessage = $"Element with Id '{element.Id}' and text '{element.Text}' was clicked.";

    [Parameter]
    public bool UseStandardTextarea { get; set; } = false;
}
</details>

Parsing line changes and contextual styles

This code demonstration shows the following.

Your razor file.

<style>
    .at-contact {
        background-color: lightcyan;
    }
</style>
<StyledTextArea
    @bind-Value=text
    StyleRules=styleRules 
    OnLinesChanged=MyOnLinesChange
    UseStandardTextarea=UseStandardTextarea>
</StyledTextArea>
@code {
    private string text = StyledTextArea.PrepareText("DemoTextChangedLinesUsage.txt".ReadResource());

    private List<StyleRule> styleRules = new List<StyleRule>()
    {
        StyleRule.Words("Maintaining Style Rules", "bold")
    };

    public async Task MyOnLinesChange(ChangedLines changedLines)
    {
        //capture the style rules named types that expired to allow you to efficiently only process for those types later
        var expiredNamedRuleTypes = changedLines.ExpiredStyleRules.Select(x => x.Name).Distinct();

        //it is important to remove all expired rules to avoid duplicates being created or reinstated
        changedLines.ExpiredStyleRules.ForEach(x => styleRules.Remove(x));

        //reprocess only changed lines with your apps logic
        //hint: process asynchronously starting with the current line being edited for most reponsive UI        
        for (var i = changedLines.StartIndex; i < changedLines.StartIndex + changedLines.Length; i++)
        {
            //we should remove style rules that we will be recreating (your app logic may improve upon this).
            //hint: it may be easier to parse the line for all rules and ignore named rule types
            styleRules.RemoveAll(x => x.LineIndex == i);

            //apply rules again
            styleRules.AddRange(TransientStyleRules(changedLines.Lines[i], i, expiredNamedRuleTypes!));
        }

        await Task.CompletedTask;
    }

    //this is your app logic to parse the text to create rules about text styling
    private List<StyleRule> TransientStyleRules(string line, int index, IEnumerable<String> expiredRuleTypes)
    {
        var firstParse = !expiredRuleTypes.Any();

        var result = new List<StyleRule>();

        if (firstParse || expiredRuleTypes.Contains("contact"))
        {
            //find '@contact' occurences and create a style rule for that line
            var contacts = line.Split(' ').Where(x => x.StartsWith("@") && x.Length > "@".Length && !x.Contains("@@"));
            var rules = contacts.Select(x => StyleRule.Words(x, "at-contact rounded").Named("contact").OnLine(index));
            result.AddRange(rules);
        }

        return result;
    }

    [Parameter]
    public bool UseStandardTextarea { get; set; } = false;
}

Typeahead Features - Inline

This code demonstration shows the following.

Demo

Your razor file.

<style>
    .typeahead {
        opacity: 0.5;
    }

    .typeahead:hover {
        opacity: 1;
        cursor: pointer;
    }
</style>
<StyledTextArea 
    @ref=textArea 
    @bind-Value=text 
    StyleRules=styleRules
    OnCaretChanged=MyOnCaretChange
    OnCalculateTypeahead=MySetTypeahead
    OnTypeaheadKeyDown=MyOnTypeaheadKeyDown
    UseStandardTextarea=UseStandardTextarea>
    <InlineTypeahead>
        @if (typeaheadSuggestion.Any())
        {
            <span class='typeahead' @onclick=AcceptInlineSuggestion>
                @typeaheadSuggestion
            </span>
        }
    </InlineTypeahead>
</StyledTextArea>
@code {
    //reference to the component is needed to call a number of instance methods
    private StyledTextArea? textArea;

    private List<StyleRule> styleRules = new List<StyleRule>()
    {
        StyleRule.Words("Typeahead Features", "bold")
    };

    private string text = StyledTextArea.PrepareText("DemoTextTypeaheadInline.txt".ReadResource());

    //callback event provides updated caret data with index, row, col, top, left, typed word used to position custom typeahead suggestions dropdown
    private CaretData? caretData { get; set; }
    private void MyOnCaretChange(CaretData caretData) => this.caretData = caretData;

    //callback event to calculate any typeahead suggestion based on text being typed
    private string typeaheadSuggestion = "";
    private void MySetTypeahead(CalculateTypeaheadEventData e)
    {
        //these are calculated to populate the custom typeahead dropdown
        var typeaheadOptions = Suggestions(e);

        //the component can recommended stopping typeahead. i.e. Escape or Delete being pressed.
        //The DefaultSuggestion is a common situation where the typeahead is the remainder of the suggesiton less the typed characters
        typeaheadSuggestion = e.RecommendPauseAndClearTypeahead ? "" 
                            : e.DefaultSuggestion(typeaheadOptions.FirstOrDefault() ?? "");
    }

    //your apps logic to use word currently being typed to derive suggestions to choose from
    private IEnumerable<string> Suggestions(CalculateTypeaheadEventData typeaheadContextData)
    {
        var partial = typeaheadContextData.WordTextToCaret;

        var options = "apple,banana,pineapple".Split(',');
        return options
            .Where(x=>x.StartsWith(partial, StringComparison.OrdinalIgnoreCase))
            .Concat(options.Where(x=>x.Contains(partial, StringComparison.OrdinalIgnoreCase)))
            .Where(x => !x.Equals(partial, StringComparison.OrdinalIgnoreCase))
            .Distinct();

        //Other useful contextual data
        //typeaheadContextData.AtEndOfLine - useful if you only want typeahead at the end of a line
        //typeaheadContextData.NextChar - char to the right of the caret
        //typeaheadContextData.RecommendPauseAndClearTypeahead - i.e. Escape or Delete and other contexts
        //typeaheadContextData.ShouldShowTypeahead - use to show or hide typeahead, blank your suggestions depending on this
    }

    //general key down handler commonly used here to interact with typeahead behaviour
    private async Task MyOnTypeaheadKeyDown(KeyboardEventArgs args)
    {
        //only useful if typeahead is active
        if (typeaheadSuggestion.Any())
        {
            //a key combination to accept current typeahead suggestion
            if (args.Key == "ArrowRight" && args.ShiftKey)
            {
                await AcceptInlineSuggestion();
            }
        }
    }

    //your reusable method to accept the current typeahead suggestion
    private async Task AcceptInlineSuggestion()
        => await AcceptTypeaheadSuggestion(caretData!.WordTextToCaret + typeaheadSuggestion);

    //your reusable method to accept the current typeahead suggestion
    private async Task AcceptTypeaheadSuggestion(string suggestion)
    {
        //replaces the currently typed word.  You can als use the InsertText method at the caret position.
        await textArea!.ReplaceWord(suggestion);

        //remember to clear the current typeahead suggestion until needed.
        typeaheadSuggestion = "";
    }

    [Parameter]
    public bool UseStandardTextarea { get; set; } = false;
}

Typeahead Features - Custom Selector

This code demonstration shows the following.

Demo

Your razor file.

<StyledTextArea @ref=textArea 
    @bind-Value=text 
    StyleRules=styleRules
    OnCaretChanged=MyOnCaretChange
    OnCalculateTypeahead=MySetTypeahead
    OnTypeaheadKeyDown=MyOnTypeaheadKeyDown
    TypeaheadReservedKeys=myTypeaheadReservedKeys
    UseStandardTextarea=UseStandardTextarea>
    <CustomTypeahead>
        @if (typeaheadOptions.Any())
        {
            <div style='position: absolute; top: @((caretData!.WordTextStartTop+20)+"px"); left: @(caretData.WordTextStartLeft+"px")'>
                <select @ref="selector" size="@typeaheadOptions.Count()" @onchange=SelectionChange @onkeydown=SelectorEnterPressed @onblur=SelectorBlur>
                    @foreach (var option in typeaheadOptions)
                    {
                        <option @onclick="_ => AcceptTypeaheadSuggestion(option)">@option</option>
                    }
                </select>   
            </div>
        }
    </CustomTypeahead>
</StyledTextArea>
@code {
    //reference to the component is needed to call a number of instance methods
    private StyledTextArea? textArea;

    //reference to the custom dropdown needed to give it focus and manage reserved key args
    private ElementReference selector;

    //currently selected typeahead suggestion option used when accepting suggestion from dropdown
    private string? selectedOption;

    //needed to stop textarea receiving these keypresses if custom typeahead dropdown is to be used
    private List<KeyboardEventArgs> myTypeaheadReservedKeys = new List<KeyboardEventArgs>() {
        new KeyboardEventArgs { Key = "ArrowDown", ShiftKey = true },
        new KeyboardEventArgs { Key = "Enter" }
    };

    private List<StyleRule> styleRules = new List<StyleRule>()
    {
        StyleRule.Words("Typeahead Features", "bold")
    };

    private string text = StyledTextArea.PrepareText("DemoTextTypeaheadSelector.txt".ReadResource());

    //callback event provides updated caret data with index, row, col, top, left, typed word used to position custom typeahead suggestions dropdown
    private CaretData? caretData { get; set; }
    private void MyOnCaretChange(CaretData caretData) => this.caretData = caretData;

    //callback event to calculate any typeahead suggestion based on text being typed
    private IEnumerable<string> typeaheadOptions = new List<string>();
    private void MySetTypeahead(CalculateTypeaheadEventData typeaheadContextData) 
    {
        var partial = typeaheadContextData.WordTextToCaret;

        //the component can recommended stopping typeahead. i.e. Escape or Delete being pressed.
        if (!partial.Any() || typeaheadContextData.RecommendPauseAndClearTypeahead)
        {
            //clear the typeahead selector from showing
            typeaheadOptions = new List<string>();
        }
        else
        {
            //your apps logic to provide typeahead selector options
            typeaheadOptions = "apple,banana,pineapple".Split(',')
                .Where(x => !x.Equals(partial, StringComparison.OrdinalIgnoreCase))
                .Where(x => x.Contains(partial, StringComparison.OrdinalIgnoreCase));
        }

        //Other useful contextual data
        //typeaheadContextData.AtEndOfLine - useful if you only want typeahead at the end of a line
        //typeaheadContextData.NextChar - char to the right of the caret
        //typeaheadContextData.RecommendPauseAndClearTypeahead - i.e. Escape or Delete and other contexts
        //typeaheadContextData.ShouldShowTypeahead - use to show or hide typeahead, blank your suggestions depending on this
    }

    //general key down handler commonly used here to interact with typeahead behaviour
    private async Task MyOnTypeaheadKeyDown(KeyboardEventArgs args)
    {
        //only useful if typeahead is active
        if (typeaheadOptions.Any())
        {
            //a key combination to move focus to the custom dropdown typeahead suggestion selector
            if (args.Key == "ArrowDown" && textArea!.IsTypeaheadReservedKey(args))
            {
                await selector.FocusAsync();

                //Enter key behaviour needs to be replaced for our implementation so need to prevent the default behaviour on the dropdown
                var keyArgs = new List<KeyboardEventArgs> { new KeyboardEventArgs { Key = "Enter" } };
                await textArea.PreventDefaultOnKeyDown(selector, keyArgs);
            }
        }
    }

    //important to catch the change to the typeahead suggestion chosen option
    private void SelectionChange(ChangeEventArgs e)
    {
        selectedOption = e.Value!.ToString();
    }

    //used to accept dropdown selected typeahead suggestion
    private async Task SelectorEnterPressed(KeyboardEventArgs e)
    {
        if (e.Code == "Enter" || e.Code == "NumpadEnter")
        {
            await AcceptTypeaheadSuggestion(selectedOption!);
        }

        if (e.Code == "Escape")
        {
            await textArea!.Focus();
            typeaheadOptions = new List<string>();
        }
    }

    //your reusable method to accept the current typeahead suggestion
    private async Task AcceptTypeaheadSuggestion(string suggestion)
    {
        //replaces the currently typed word.  You can als use the InsertText method at the caret position.
        await textArea!.ReplaceWord(suggestion);

        //remember to clear the current typeahead suggestion until needed.
        typeaheadOptions = new List<string>();
    }

    //return focus to component otherwise custom dropdown will remain when component does not have focus
    private async Task SelectorBlur()
    {
        await textArea!.Focus();
    }

    [Parameter]
    public bool UseStandardTextarea { get; set; } = false;
}

Limitations

Blazor framework

Component

Version History

Roadmap