Edit on GitHub

Server Ui Controls

Both #Script Pages and ServiceStack.Razor Share the same implementations for their Server Controls which are utilized in ASP.NET Core Project Templates and World Validation Application.

UI Component List

Currently the component libraries include common Bootstrap UI Form Controls and Navigation Components, here’s a side-by-side comparison displaying the names for the different Control for each Server Control:

Control #Script Pages ServiceStack.Razor
ErrorSummary validationSummary @Html.ValidationSummary
ValidationSuccess validationSuccess @Html.ValidationSuccess
Input formInput @Html.FormInput
TextArea formTextarea @Html.FormTextarea
Select formSelect @Html.FormSelect
CheckBox formInput({type:’checkbox’}) @Html.FormInput(new { type = “checkbox”})
HiddenInputs htmlHiddenInputs @Html.HiddenInputs
SvgImage svgImage @Html.SvgImage
Nav nav @Html.Nav
Navbar navbar @Html.Navbar
NavLink navLink @Html.NavLink
NavButtonGroup navButtonGroup @Html.NavButtonGroup

Bootstrap UI Form Controls

The Bootstrap UI form controls include built-in support for validation where they can render validation errors from ServiceStack’s ResponseStatus object, e.g the Login Page in World Validation:

#Script Pages

<form action="/auth/credentials" method="post" class="col-lg-4">
    <div class="form-group">
        {{ ['userName','password'] | validationSummary({class:'alert alert-warning'}) }}
        {{ { continue: qs.continue ?? '/server/', errorView:'/server/login' } | htmlHiddenInputs }}
    </div>
    <div class="form-group">
        {{ {id:'userName'} 
           | formInput({label:'Email',help:'Email you signed up with',size:'lg'}) }}
    </div>
    <div class="form-group">
        {{ {id:'password',type:'password'} 
           | formInput({label:'Password',help:'6 characters or more',size:'lg',preserveValue:false}) }}
    </div>
    <div class="form-group">
        {{ {id:'rememberMe',type:'checkbox',checked:true} | formInput({label:'Remember Me'}) }}
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-lg btn-primary">Login</button>
    </div>
    <div class="form-group">
        <a class="lnk" href="/server/register">Register New User</a>
    </div>
</form>

ServiceStack.Razor

The equivalent implementation in Razor:

<form action="/auth/credentials" method="post" class="col-lg-4">
    <div class="form-group">
        @Html.ValidationSummary(new[]{ "userName","password" }, 
            new { @class = "alert alert-warning" })
        
        @Html.HiddenInputs(new { 
            @continue = Html.Query("continue") ?? "/server-razor/",
            errorView = "/server-razor/login"
        })
    </div>
    <div class="form-group">
        @Html.FormInput(new { id = "userName" }, new InputOptions {
            Label = "Email",
            Help  = "Email you signed up with",
            Size  = "lg",
        })
    </div>
    <div class="form-group">
        @Html.FormInput(new { id = "password", type = "password" }, new InputOptions {
            Label = "Password",
            Help  = "6 characters or more",
            Size  = "lg",
            PreserveValue = false,
        })
    </div>
    <div class="form-group">
        @Html.FormInput(new { 
            id   = "rememberMe", 
            type = "checkbox",
            @checked = true,
        },
        new InputOptions { Label = "Remember Me" })
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-lg btn-primary">Login</button>
    </div>
    <div class="form-group">
        <a class="lnk" href="/server-razor/register">Register New User</a>
    </div>
</form>

Login Page UI

The Login Page contains a standard Bootstrap Username/Password form with labels, placeholders and help text, which initially looks like:

What it looks like after submitting an empty form with Server Exception Errors rendered against their respective fields:

Form Control Properties

Essentially both #Script and Razor have identical properties but implemented idiomatically for each control where #Script uses camelCase names and JS Object literals. The first (aka target) argument is for attributes you want to add to the HTML <input/> Element whilst the 2nd Argument is used to customize any of its other high-level features:

/// High-level Input options for rendering HTML Input controls
public class InputOptions
{
    /// Display the Control inline 
    public bool Inline { get; set; }
    
    /// Label for the control
    public string Label { get; set; }
    
    /// Class for Label
    public string LabelClass { get; set; }
    
    /// Override the class on the error message (default: invalid-feedback)
    public string ErrorClass { get; set; }

    /// Small Help Text displayed with the control
    public string Help { get; set; }
    
    /// Bootstrap Size of the Control: sm, lg
    public string Size { get; set; }
    
    /// Multiple Value Data Source for Checkboxes, Radio boxes and Select Controls 
    public object Values { get; set; }

    /// Typed setter of Multi Input Values
    public IEnumerable<KeyValuePair<string, string>> InputValues
    {
        set => Values = value;
    }

    /// Whether to preserve value state after post back
    public bool PreserveValue { get; set; } = true;

    /// Whether to show Error Message associated with this control
    public bool ShowErrors { get; set; } = true;
}

Contacts Page

The Contacts Page shows a more complete example with a number of different UI Controls.

#Script Pages

<form action="/contacts" method="post" class="col-lg-4">
    <div class="form-group">
        {{ 'title,name,color,age,filmGenres,agree' | validationSummary }}
    </div>
    <div class="form-group">
        {{ {id:'title',type:'radio'} | formInput({values:contactTitles,inline:true}) }}
    </div>
    <div class="form-group">
        {{ {id:'name',placeholder:'Name'} | formInput({label:'Full Name',help:'Your first and last name'}) }}
    </div>
    <div class="form-group">
        {{ {id:'color',class:'col-4'}
           | formSelect({label:'Favorite color',values:{'', ...contactColors}}) }}
    </div>
    <div class="form-group">
        {{ {id:'filmGenres',type:'checkbox'} 
          | formInput({label:'Favorite Film Genres',values:contactGenres,help:"choose one or more"}) }}
    </div>
    <div class="form-group">
        {{ {id:'age',type:'number',min:13,placeholder:'Age',class:'col-3'} | formInput }}
    </div>
    <div class="form-group">
        {{ {id:'agree',type:'checkbox'} | formInput({label:'Agree to terms and conditions'}) }}
    </div>
    <div class="form-group">
        <button class="btn btn-primary" type="submit">Add Contact</button>
        <a href="/server/contacts/">reset</a>
    </div>
</form>

Whereas Razor uses anonymous objects for properties that can be unbounded like a HTML Element Attribute List and a Typed Class like InputOptions to specify the controls other high-level features, e.g:

ServiceStack.Razor

<form action="/contacts" method="post" class="col-lg-4">
    <div class="form-group">
        @Html.ValidationSummary(new[]{ "title","name","color","age","filmGenres","agree" })
        @Html.HiddenInputs(new { @continue = Continue, errorView = Continue })
    </div>
    <div class="form-group">
        @Html.FormInput(new { 
            id = "title", 
            type = "radio",
        }, new InputOptions { 
            Values = Html.ContactTitles(),
            Inline = true,
        })
    </div>
    <div class="form-group">
        @Html.FormInput(new { 
            id = "name", 
            placeholder = "Name", 
        }, new InputOptions {
            Label = "Full Name",
            Help  = "Your first and last name",
        })
    </div>
    <div class="form-group">
        @Html.FormSelect(new { 
            id = "color", 
            @class = "col-4", 
        }, new InputOptions {
            Label  = "Favorite color",
            Values = new StringDictionary { {"",""} }.Merge(Html.ContactColors()),
        })
    </div>
    <div class="form-group">
        @Html.FormInput(new { 
            id = "filmGenres", 
            type = "checkbox",
        }, new InputOptions { 
            Label  = "Favorite Film Genres",
            Help   = "choose one or more",
            Values = Html.ContactGenres()
        })
    </div>
    <div class="form-group">
        @Html.FormInput(new { 
            id = "age", 
            type = "number",
            min = 13,
            placeholder = "Age",
            @class = "col-3",
        })
    </div>
    <div class="form-group">
        @Html.FormInput(new {
            id   = "agree",
            type = "checkbox",
        },
        new InputOptions { Label = "Agree to terms and conditions" })
    </div>
    <div class="form-group">
        <button class="btn btn-primary" type="submit">Add Contact</button>
        <a href="/server-razor/contacts/">reset</a>
    </div>
</form>

Both Server UI Controls provide auto Validation Form Binding for any validation rules specified on the CreateContact Validator:

public class CreateContactValidator : AbstractValidator<CreateContact>
{
    public CreateContactValidator()
    {
        RuleFor(r => r.Title).NotEqual(Title.Unspecified).WithMessage("Please choose a title");
        RuleFor(r => r.Name).NotEmpty();
        RuleFor(r => r.Color).Must(x => x.IsValidColor()).WithMessage("Must be a valid color");
        RuleFor(r => r.FilmGenres).NotEmpty().WithMessage("Please select at least 1 genre");
        RuleFor(r => r.Age).GreaterThan(13).WithMessage("Contacts must be older than 13");
        RuleFor(x => x.Agree).Equal(true).WithMessage("You must agree before submitting");
    }
}

As well as any ArgumentException thrown within the Service Implementation:

public object Any(CreateContact request) 
{
    var newContact = request.ConvertTo<Data.Contact>();
    newContact.Id = Interlocked.Increment(ref Counter);
    newContact.UserAuthId = this.GetUserId();
    newContact.CreatedDate = newContact.ModifiedDate = DateTime.UtcNow;

    var contacts = Contacts.Values.ToList();
    var alreadyExists = contacts.Any(x => x.UserAuthId == newContact.UserAuthId && x.Name == request.Name);
    if (alreadyExists)
        throw new ArgumentException($"You already have contact named '{request.Name}'",nameof(request.Name));
    
    Contacts[newContact.Id] = newContact;
    return new CreateContactResponse { Result = newContact.ConvertTo<Contact>() };
}

Contacts Page UI

The Contacts Page is representative of a more complex page that utilizes a variety of different form controls where the same page is also responsible for rendering the list of existing contacts:

Here’s an example of what a partially submitted invalid form looks like:

To view the complete implementation in context checkout World Validation Server Implementation.

The Server Navigation Controls are used to render your Apps Unified Navigation

#Script Pages

In #Script Pages you can use render the navbar and navButtonGroup methods to render NavItems:

You can render the main menu navigation using the navbar script method:

{{ navbar }}

Which by default renders the View.NavItems main navigation, using the default NavOptions and User Attributes (if authenticated):

You can also render a different Navigation List with:

{{ navItems('submenu').navbar() }}

Which can be customized using the different NavOptions properties above, in camelCase:

{{ navItems('submenu').navbar({ navClass: 'navbar-nav navbar-light bg-light' }) }}

Button group

The navButtonGroup script can render NavItems in a button group, e.g. the OAuth buttons are rendered with:

{{ 'auth'.navItems().navButtonGroup({ navClass: '', navItemClass: 'btn btn-block btn-lg' }) }}

Which renders a vertical, spaced list of buttons which look like:

Razor Pages

The same server controls are available in ServiceStack.Razor Apps as HTML Helper extension methods:

@Html.Navbar()

@Html.Navbar(Html.GetNavItems("submenu"))

@Html.Navbar(Html.GetNavItems("submenu"), new NavOptions {
    NavClass = "navbar-nav navbar-light bg-light"
})
@Html.NavButtonGroup(Html.GetNavItems("auth"), new NavOptions {
    NavClass = "",
    NavItemClass = "btn btn-block btn-lg",
})

The NavItem classes capture the Navigation information which is used together with the NavOptions class below:

public class NavOptions
{
    /// User Attributes for conditional rendering, e.g:
    ///  - auth - User is Authenticated
    ///  - role:name - User Role
    ///  - perm:name - User Permission 
    public HashSet<string> Attributes { get; set; }
    
    /// Path Info that should set as active 
    public string ActivePath { get; set; }
    
    /// Prefix to include before NavItem.Path (if any)
    public string BaseHref { get; set; }

    // Custom classes applied to different navigation elements (defaults to Bootstrap classes)
    public string NavClass { get; set; }
    public string NavItemClass { get; set; }
    public string NavLinkClass { get; set; }
    
    public string ChildNavItemClass { get; set; }
    public string ChildNavLinkClass { get; set; }
    public string ChildNavMenuClass { get; set; }
    public string ChildNavMenuItemClass { get; set; }
}

Use camelCase when specifying nav options in #Script controls.