Edit on GitHub

World Validation

The World Validation App covers a typical App example you’d find in most Apps, including Login and Registration Forms to Sign In and Register new Users who are then able to access the same protected Services to maintain their own private contact lists. It’s a compact example that tries to cover a lot of use-cases typical in a real-world App, including maintaining a separate Data and DTO Model and using C# idioms like Enum’s for defining a finite list of options which are re-used to populate its HTML UI.

The UI for the same App is re-implemented in 10 popular Web Development approaches, each integrated with ServiceStack’s validation.

As of this writing there 4 different server HTML generated strategies that use HTML Form Posts to call back-end Services:

View Source on GitHub NetCoreApps/Validation - Live Demo validation.web-app.io

Server Rendered HTML UIs

Client HTML UIs

The Client Examples use Ajax Forms and the TypeScript JsonServiceClient to send TypeScript dtos.ts generated with TypeScript Add ServiceStack Reference:

The source code for all different strategies are encapsulated within their folders above except for the Razor examples which need to maintain their shared resources in the /Views folder (representative of friction and restrictions when working with Razor).

Server Implementation

This is the shared backend Server implementation that all UIs are using:

All Auth Configuration is encapsulated within a “no-touch” IConfigureAppHost plugin that’s run once on Startup:

// Run before AppHost.Configure()
public class ConfigureAuth : IConfigureAppHost
{
public void Configure(IAppHost appHost)
{
var AppSettings = appHost.AppSettings;
appHost.Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(), //Enable UserName/Password Credentials Auth
}));
appHost.Plugins.Add(new RegistrationFeature()); //Enable /register Service
//override the default registration validation with your own custom implementation
appHost.RegisterAs<CustomRegistrationValidator, IValidator<Register>>();
container.Register<ICacheClient>(new MemoryCacheClient()); //Store User Sessions in Memory
appHost.Register<IAuthRepository>(new InMemoryAuthRepository()); //Store Authenticated Users in Memory
CreateUser(appHost, "admin@email.com", "Admin User", "p@55wOrd", roles:new[]{ RoleNames.Admin });
}
// Add initial Users to the configured Auth Repository
public void CreateUser(IAppHost appHost, string email, string name, string password, string[] roles)
{
var authRepo = appHost.TryResolve<IAuthRepository>();
var newAdmin = new UserAuth { Email = email, DisplayName = name };
var user = authRepo.CreateUserAuth(newAdmin, password);
authRepo.AssignRoles(user, roles);
}
}
// Type class to store additional metadata in Users Session
public class CustomUserSession : AuthUserSession {}
// Custom Validator to add custom validators to built-in /register Service requiring DisplayName and ConfirmPassword
public class CustomRegistrationValidator : RegistrationValidator
{
public CustomRegistrationValidator()
{
RuleSet(ApplyTo.Post, () =>
{
RuleFor(x => x.DisplayName).NotEmpty();
RuleFor(x => x.ConfirmPassword).NotEmpty();
});
}
}
view raw Configure.Auth.cs hosted with ❤ by GitHub

All Services and Validators used in this App. Extension methods are used to DRY reusable code and a Custom Auto Mapping handles conversion between the Contact Data Model and Contact`` DTO:

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");
}
}
[Authenticate] // Limit to Authenticated Users
[ErrorView(nameof(CreateContact.ErrorView))] // Display ErrorView for HTML requests resulting in an Exception
[DefaultView("/server/contacts")] // Render custom HTML View for HTML Requests
public class ContactServices : Service
{
private static int Counter = 0;
internal static ConcurrentDictionary<int, Data.Contact> Contacts = new ConcurrentDictionary<int, Data.Contact>();
public object Any(GetContacts request)
{
var userId = this.GetUserId();
return new GetContactsResponse
{
Results = Contacts.Values
.Where(x => x.UserAuthId == userId)
.OrderByDescending(x => x.Id)
.Map(x => x.ConvertTo<Contact>())
};
}
public object Any(GetContact request) =>
Contacts.TryGetValue(request.Id, out var contact) && contact.UserAuthId == this.GetUserId()
? (object)new GetContactResponse { Result = contact.ConvertTo<Contact>() }
: HttpError.NotFound($"Contact was not found");
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 a contact named '{request.Name}'", nameof(request.Name));
Contacts[newContact.Id] = newContact;
return new CreateContactResponse { Result = newContact.ConvertTo<Contact>() };
}
public object AnyHtml(CreateContact request) // Called for CreateContact API HTML Requests on any HTTP Method
{
Any(request);
return HttpResult.Redirect(request.Continue ?? Request.GetView());
}
public void Any(DeleteContact request)
{
if (Contacts.TryGetValue(request.Id, out var contact) && contact.UserAuthId == this.GetUserId())
Contacts.TryRemove(request.Id, out _);
}
public object PostHtml(DeleteContact request) // Only called by DeleteContact HTML POST requests
{
Any(request);
return HttpResult.Redirect(request.Continue ?? Request.GetView()); //added by [DefaultView]
}
}
public class UpdateContactValidator : AbstractValidator<UpdateContact>
{
public UpdateContactValidator()
{
RuleFor(r => r.Id).GreaterThan(0);
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");
}
}
[ErrorView(nameof(UpdateContact.ErrorView))] // Display ErrorView for HTML requests resulting in an Exception
public class UpdateContactServices : Service
{
public object Any(UpdateContact request)
{
if (!ContactServices.Contacts.TryGetValue(request.Id, out var contact) || contact.UserAuthId != this.GetUserId())
throw HttpError.NotFound("Contact was not found");
contact.PopulateWith(request);
contact.ModifiedDate = DateTime.UtcNow;
return request.Continue != null
? (object) HttpResult.Redirect(request.Continue)
: new UpdateContactResponse();
}
}
public static class ContactServiceExtensions // DRY reusable logic used in Services and Validators
{
public static int GetUserId(this Service service) => int.Parse(service.GetSession().UserAuthId);
public static bool IsValidColor(this string color) => !string.IsNullOrEmpty(color) &&
(color.FirstCharEquals('#')
? int.TryParse(color.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)
: Color.FromName(color).IsKnownColor);
}
// Register Custom Auto Mapping for converting Contact Data Model to Contact DTO
public class ContactsHostConfig : IConfigureAppHost
{
public void Configure(IAppHost appHost) =>
AutoMapping.RegisterConverter((Data.Contact from) => from.ConvertTo<Contact>(skipConverters:true));
}
view raw ContactServices.cs hosted with ❤ by GitHub

The dynamic App data used within ServiceStack Sharp Pages and Razor pages are maintained within Custom ContactScripts and RazorHelpers:

// Custom filters for App data sources and re-usable UI snippets in ServiceStack Sharp Pages
public class ContactScripts : ScriptMethods
{
internal readonly List<KeyValuePair<string, string>> MenuItems = new List<KeyValuePair<string, string>> {
KeyValuePair.Create("/", "Home"),
KeyValuePair.Create("/login-links", "Login Links"),
KeyValuePair.Create("/register-links", "Register Links"),
KeyValuePair.Create("/contact-links", "Contacts Links"),
KeyValuePair.Create("/contact-edit-links", "Edit Contact Links"),
};
public List<KeyValuePair<string, string>> menuItems() => MenuItems;
static Dictionary<string, string> Colors = new Dictionary<string, string> {
{"#ffa4a2", "Red"},
{"#b2fab4", "Green"},
{"#9be7ff", "Blue"}
};
public Dictionary<string, string> contactColors() => Colors;
private static List<KeyValuePair<string, string>> Titles => EnumUtils.GetValues<Title>()
.Where(x => x != Title.Unspecified)
.ToKeyValuePairs();
public List<KeyValuePair<string, string>> contactTitles() => Titles;
private static List<string> FilmGenres => EnumUtils.GetValues<FilmGenres>().Map(x => x.ToDescription());
public List<string> contactGenres() => FilmGenres;
}
// Razor Helpers for App data sources and re-usable UI snippets in Razor pages
public static class RazorHelpers
{
internal static readonly ContactScripts Instance = new ContactScripts();
public static Dictionary<string, string> ContactColors(this IHtmlHelper html) => Instance.contactColors();
public static List<KeyValuePair<string, string>> ContactTitles(this IHtmlHelper html) => Instance.contactTitles();
public static List<string> ContactGenres(this IHtmlHelper html) => Instance.contactGenres();
public static List<KeyValuePair<string, string>> MenuItems(this IHtmlHelper html) => Instance.MenuItems;
}
view raw Contact.UI.cs hosted with ❤ by GitHub

Typed Request/Response Service Contracts including Data and DTO models that utilizes Enum’s:

namespace Data // DB Models
{
using ServiceModel.Types;
public class Contact // Data Model
{
public int Id { get; set; }
public int UserAuthId { get; set; }
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
}
}
namespace ServiceModel // Request/Response DTOs
{
using Types;
[Route("/contacts", "GET")]
public class GetContacts : IReturn<GetContactsResponse> {}
public class GetContactsResponse
{
public List<Contact> Results { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts/{Id}", "GET")]
public class GetContact : IReturn<GetContactResponse >
{
public int Id { get; set; }
}
public class GetContactResponse
{
public Contact Result { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts", "POST")]
public class CreateContact : IReturn<CreateContactResponse>
{
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
public bool Agree { get; set; }
public string Continue { get; set; }
public string ErrorView { get; set; }
}
public class CreateContactResponse
{
public Contact Result { get; set; }
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts/{Id}", "POST PUT")]
public class UpdateContact : IReturn<UpdateContactResponse>
{
public int Id { get; set; }
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
public string Continue { get; set; }
public string ErrorView { get; set; }
}
public class UpdateContactResponse
{
public ResponseStatus ResponseStatus { get; set; }
}
[Route("/contacts/{Id}", "DELETE")]
[Route("/contacts/{Id}/delete", "POST")] // more accessible from HTML
public class DeleteContact : IReturnVoid
{
public int Id { get; set; }
public string Continue { get; set; }
}
namespace Types // DTO Types
{
public class Contact
{
public int Id { get; set; }
public int UserAuthId { get; set; }
public Title Title { get; set; }
public string Name { get; set; }
public string Color { get; set; }
public FilmGenres[] FilmGenres { get; set; }
public int Age { get; set; }
}
public enum Title
{
Unspecified=0,
[Description("Mr.")] Mr,
[Description("Mrs.")] Mrs,
[Description("Miss.")] Miss
}
public enum FilmGenres
{
Action,
Adventure,
Comedy,
Drama,
}
}
}
view raw Contact.Models.cs hosted with ❤ by GitHub

Each UI implements 4 different screens which are linked from:

Shared Error Handling Concepts

Despite their respective differences they share the same concepts where all validation errors are populated from the Service’s ResponseStatus Error Response. The UI implementations takes care of binding all matching field errors next to their respective controls whilst the validationSummary or errorResponseExcept methods takes a list of field names that they should not display as they’ll already be displayed next to their targeted control.

We’ll cover just the Login and Contacts Pages since they’re sufficiently different, to see what this looks like in practice:

Login Page

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:

Server UIs

All Server Examples submits a HTML Form Post and renders full page responses:

Source Code and References

<h3>Sign In using credentials</h3>
<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>
view raw login.html hosted with ❤ by GitHub

Source Code and References

<script src="/lib/@servicestack/client/index.js"></script>
view raw _layout.html hosted with ❤ by GitHub
<h3>Sign In using credentials</h3>
<form action="/auth/credentials" method="post">
<div class="form-row">
<div class="form-group">
{{ '<div class="alert alert-danger">{0}</div>' | htmlFormat(errorResponseExcept(['userName','password'])) }}
<input type="hidden" name="continue" value="{{qs.continue ?? '/server-ts/'}}" />
<input type="hidden" name="errorView" value="/server-ts/login">
</div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="userName" type="text" placeholder="UserName"
value="{{ 'userName' | formValue }}" data-invalid="{{ 'userName' | errorResponse }}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="password" type="password" placeholder="Password"
value="" data-invalid="{{ 'password' | errorResponse }}">
</div>
<div class="form-group col-md-4">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<input type="checkbox" id="rememberMe" name="rememberMe" value="true" {{ {checked:formCheckValue('rememberMe')} | htmlAttrs }}>
<label for="rememberMe">Remember Me</label>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<a class="lnk" href="/server-ts/register">Register New User</a>
</div>
</div>
</form>
{{#raw appendTo scripts}}
<script src="/server-ts/login.js"></script>
{{/raw}}
view raw login.html hosted with ❤ by GitHub
import {bindHandlers, bootstrap} from "@servicestack/client";
bootstrap(); //converts data-invalid attributes into Bootstrap v4 error messages.
bindHandlers({
switchUser: (u: string) => {
(document.querySelector("[name=userName]") as HTMLInputElement).value = u;
(document.querySelector("[name=password]") as HTMLInputElement).value = 'p@55wOrd';
}
});
view raw login.ts hosted with ❤ by GitHub

Source Code and References

<script src="/assets/js/jquery.min.js"></script>
<script src="/js/ss-utils.js"></script>
view raw _layout.html hosted with ❤ by GitHub
<h3>Sign In using credentials</h3>
<form action="/auth/credentials" method="post">
<div class="form-row">
<div class="form-group">
{{ '<div class="alert alert-danger">{0}</div>' | htmlFormat(errorResponseExcept(['userName','password'])) }}
<input type="hidden" name="continue" value="{{qs.continue ?? '/server-jquery/'}}" />
<input type="hidden" name="errorView" value="/server-jquery/login">
</div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="userName" type="text" placeholder="UserName"
value="{{ 'userName' | formValue }}" data-invalid="{{ 'userName' | errorResponse }}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="password" type="password" placeholder="Password"
value="" data-invalid="{{ 'password' | errorResponse }}">
</div>
<div class="form-group col-md-4">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<input type="checkbox" id="rememberMe" name="rememberMe" value="true" {{ {checked:formCheckValue('rememberMe')} | htmlAttrs }}>
<label for="rememberMe">Remember Me</label>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<a class="lnk" href="/server-jquery/register">Register New User</a>
</div>
</div>
</form>
<div>
<b>Quick Login:</b>
<div class="quicklist">
<span data-click="switchUser:admin@email.com">admin@email.com</span>
<span data-click="switchUser:new@user.com">new@user.com</span>
</div>
</div>
{{#raw appendTo scripts}}
<script>
$(document).bootstrap().bindHandlers({
switchUser: function(u) {
$("[name=userName]").val(u);
$("[name=password]").val('p@55wOrd');
}
});
</script>
{{/raw}}
view raw login.html hosted with ❤ by GitHub

Source Code and References

<h3>Sign In using credentials</h3>
<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>
view raw login.cshtml hosted with ❤ by GitHub

About Server Implementations

Unfortunately Validation in Bootstrap doesn’t lend itself to easy server rendering as it requires co-ordination with label, input and error feedback elements so Sharp Pages wraps this in a formInput control from BootstrapScripts to render both Label and Input elements together. For those preferring Razor, these same controls are available as @Html Helpers as seen in Server Razor which ends up having identical behavior and markup, albeit rendered using a different View Engine.

Server TypeScript shows a more fine-grained version where we show how to bind validation errors to your own custom HTML markup. This would normally end up being a lot more tedious to do so we’ve extended it with our own declarative data-invalid attribute to hold the fields error message which drastically reduces the manual binding effort required. Calling the bootstrap() method will scan the form for populated data-invalid attributes where it’s used to render the appropriate error message adjacent to the control and toggle the appropriate error classes.

All TypeScript examples only depends on the dependency-free @servicestack/client which is available as both an npm package and as a stand-alone servicestack-client.umd.js script include.

The Server jQuery version uses the exact same markup as Server TypeScript but requires a dependency on jQuery and uses the $(document).bootstrap() jQuery plugin from ServiceStack’s built-in ss-utils.js.

Continue and ErrorView

In order to enable full-page reloads in ServiceStack’s built-in Services like its /auth and /register Services we need to submit 2 additional hidden input fields: errorView to tell it which page it should render on failed requests and continue to tell it where to redirect to after successful requests.

Client UIs

In contrast to full page reloads all Client UIs submit Ajax forms and bind their JSON Error Response to the UI for a more fluid and flicker-free UX:

Source Code and References

{{#raw}}
<template>
<v-layout justify-center>
<v-flex xs5>
<v-toolbar color="pink">
<v-toolbar-title class="white--text">
Sign In using credentials
</v-toolbar-title>
</v-toolbar>
<v-card>
<v-form v-model="valid" ref="form" lazy-validation @keyup.native.enter="submit">
<v-container>
<v-alert outline color="error" icon="warning" :value="errorSummary">{{ errorSummary }}</v-alert>
<v-layout column>
<v-text-field
label="Email"
v-model="userName"
required
:rules="emailRules"
:error-messages="errorResponse('userName')"
></v-text-field>
<v-text-field
label="Password"
v-model="password"
type="password"
required
:rules="passwordRules"
:error-messages="errorResponse('password')"
></v-text-field>
<v-checkbox label="Remember Me" v-model="rememberMe"></v-checkbox>
</v-layout>
</v-container>
</v-form>
<v-card-actions>
<v-flex offset-xs2 mb-2>
<v-btn flat href="/vuetify/register" :disabled="!valid || loading">Register New User</v-btn>
<v-btn color="primary" @click="submit" :disabled="!valid || loading">Sign In</v-btn>
</v-flex>
</v-card-actions>
</v-card>
<v-flex>
<v-toolbar>
<v-toolbar-title>
Quick Login
</v-toolbar-title>
</v-toolbar>
<v-card>
<v-layout>
<v-flex>
<v-btn @click="switchUser('admin@email.com')" flat>admin@email.com</v-btn>
</v-flex>
<v-flex>
<v-btn @click="switchUser('new@user.com')" flat>new@user.com</v-btn>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<div class="src">
<h4>Source Code and References</h4>
<div><a href="https://github.com/NetCoreApps/Validation/blob/master/world/wwwroot/vuetify/_layout.html" class="ref">/vuetify/_layout.html</a></div>
<div><a href="https://github.com/NetCoreApps/Validation/blob/master/world/wwwroot/vuetify/login.html" class="ref">/vuetify/login.html</a></div>
<div><a href="https://github.com/NetCoreApps/Validation/blob/master/world/wwwroot/vuetify/login.ts" class="ref">/vuetify/login.ts</a></div>
</div>
</v-flex>
</v-layout>
</template>
{{/raw}}
{{#capture appendTo scripts}}
<script>var CONTINUE = '{{ qs.redirect ?? "/vuetify/" }}';</script>
<script src="login.js"></script>
{{/capture}}
view raw login.html hosted with ❤ by GitHub
import { Vue } from 'vue';
import { errorResponse, errorResponseExcept } from '@servicestack/client';
import { client, emailRules, passwordRules } from './shared';
import { Authenticate } from "../dtos";
declare var CONTINUE:any;
new Vue({
el: '#app',
computed: {
errorSummary: function() {
return errorResponseExcept.call(this, 'userName,password');
},
},
methods: {
async submit() {
const form = (this.$refs.form as HTMLFormElement);
if (form.validate()) {
try {
this.loading = true;
const response = await client.post(new Authenticate({
provider: 'credentials',
userName: this.userName,
password: this.password,
rememberMe: this.rememberMe,
}));
location.href = CONTINUE;
} catch (e) {
this.responseStatus = e.responseStatus || e;
} finally {
this.loading = false;
form.resetValidation();
}
}
},
switchUser(email:string) {
this.userName = email;
this.password = 'p@55wOrd';
},
errorResponse
},
data: () => ({
loading: false,
valid: true,
userName: "",
password: "",
rememberMe: true,
emailRules, passwordRules,
responseStatus: null
}),
});
view raw login.ts hosted with ❤ by GitHub

Source Code and References

<script src="/lib/@servicestack/client/index.js"></script>
<script src="/dtos.js"></script>
view raw _layout.html hosted with ❤ by GitHub
<h3>Sign In using credentials</h3>
<form action="/auth/credentials" method="post">
<div class="form-row">
<div class="form-group" data-validation-summary="userName,password"></div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="userName" type="text" placeholder="UserName">
</div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="password" type="password" placeholder="Password">
</div>
<div class="form-group col-md-4">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<input type="checkbox" id="rememberMe" name="rememberMe" value="true">
<label for="rememberMe">Remember Me</label>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<a class="lnk" href="/client-ts/register">Register New User</a>
</div>
</div>
</form>
<div>
<b>Quick Login:</b>
<div class="quicklist">
<span data-click="switchUser:admin@email.com">admin@email.com</span>
<span data-click="switchUser:new@user.com">new@user.com</span>
</div>
</div>
{{#capture appendTo scripts}}
<script>var CONTINUE = '{{qs.continue ?? "/client-ts/"}}';</script>
<script src="login.js"></script>
{{/capture}}
view raw login.html hosted with ❤ by GitHub
import { bindHandlers, bootstrapForm } from "@servicestack/client";
import { AuthenticateResponse } from "../../dtos";
declare var CONTINUE:string;
bootstrapForm(document.querySelector('form'), {
success: (r: AuthenticateResponse) => {
location.href = CONTINUE;
}
});
bindHandlers({
switchUser: (u: string) => {
(document.querySelector("[name=userName]") as HTMLInputElement).value = u;
(document.querySelector("[name=password]") as HTMLInputElement).value = 'p@55wOrd';
}
});
view raw login.ts hosted with ❤ by GitHub

Source Code and References

<script src="/assets/js/jquery.min.js"></script>
<script src="/js/ss-utils.js"></script>
view raw _layout.html hosted with ❤ by GitHub
<h3>Sign In using credentials</h3>
<form action="/auth/credentials" method="post">
<div class="form-row">
<div class="form-group" data-validation-summary="userName,password"></div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="userName" type="text" placeholder="UserName">
</div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="password" type="password" placeholder="Password">
</div>
<div class="form-group col-md-4">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<input type="checkbox" id="rememberMe" name="rememberMe" value="true">
<label for="rememberMe">Remember Me</label>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<a class="lnk" href="/client-ts/register">Register New User</a>
</div>
</div>
</form>
<div>
<b>Quick Login:</b>
<div class="quicklist">
<span data-click="switchUser:admin@email.com">admin@email.com</span>
<span data-click="switchUser:new@user.com">new@user.com</span>
</div>
</div>
{{#capture appendTo scripts}}
<script>var CONTINUE = '{{qs.continue ?? "/client-jquery/"}}';</script>
{{/capture}}
{{#raw appendTo scripts}}
<script>
$('form').bootstrapForm({
success: function(r) {
location.href = CONTINUE;
}
});
$(document).bindHandlers({
switchUser: function (u) {
$("[name=userName]").val(u);
$("[name=password]").val('p@55wOrd');
}
});
</script>
{{/raw}}
view raw login.html hosted with ❤ by GitHub

Source Code and References

<script src="/assets/js/jquery.min.js"></script>
<script src="/js/ss-utils.js"></script>
view raw _Layout.cshtml hosted with ❤ by GitHub
<h3>Sign In using credentials</h3>
<form action="/auth/credentials" method="post">
<div class="form-row">
<div class="form-group" data-validation-summary="userName,password"></div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="userName" type="text" placeholder="UserName">
</div>
</div>
<div class="form-row">
<div class="form-group">
<input class="form-control form-control-lg" name="password" type="password" placeholder="Password">
</div>
<div class="form-group col-md-4">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<input type="checkbox" id="rememberMe" name="rememberMe" value="true">
<label for="rememberMe">Remember Me</label>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<a class="lnk" href="/client-razor/register">Register New User</a>
</div>
</div>
</form>
<div>
<b>Quick Login:</b>
<div class="quicklist">
<span data-click="switchUser:admin@email.com">admin@@email.com</span>
<span data-click="switchUser:new@user.com">new@@user.com</span>
</div>
</div>
@section scripts {
<script>
var CONTINUE = '@(Html.Query("continue") ?? "/client-razor/")';
$('form').bootstrapForm({
success: function(r) {
location.href = CONTINUE;
}
});
$(document).bindHandlers({
switchUser: function (u) {
$("[name=userName]").val(u);
$("[name=password]").val('p@55wOrd');
}
});
</script>
}
view raw login.cshtml hosted with ❤ by GitHub

Source Code and References

<div id="page"></div>
{{#raw appendTo scripts}}
<script src="login.js"></script>
<script>
new SignIn({
el: '#page'
})
</script>
{{/raw}}
view raw login.html hosted with ❤ by GitHub
import { Vue, Component, Watch } from 'vue-property-decorator'
import { bus, client, Authenticate, redirect } from './shared'
@Component({ template:
`<div>
<h3>Sign In</h3>
<form ref="form" @submit.prevent="submit" :class="{ error:responseStatus, loading }" >
<div class="form-group">
<ErrorSummary except="userName,password" :responseStatus="responseStatus" />
</div>
<div class="form-group">
<Input name="userName" v-model="userName" placeholder="Username" :responseStatus="responseStatus" />
</div>
<div class="form-group">
<Input type="password" name="password" v-model="password" placeholder="Password" :responseStatus="responseStatus" />
</div>
<div class="form-group">
<CheckBox name="rememberMe" v-model="rememberMe" :responseStatus="responseStatus">
Remember Me
</CheckBox>
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary">Login</button>
</div>
<div class="form-group">
<a class="btn btn-outline-primary" href="/client-vue/register">Register New User</a>
</div>
</form>
<div class="pt-3">
<b>Quick Login:</b>
<p class="pt-1">
<a class="btn btn-outline-info btn-sm" href="javascript:void(0)" @click.prevent="switchUser('admin@email.com')">admin@email.com</a>
<a class="btn btn-outline-info btn-sm" href="javascript:void(0)" @click.prevent="switchUser('new@user.com')">new@user.com</a>
</p>
</div>
</div>`,
props: {
redirect: String
}
})
export class SignIn extends Vue {
userName = ''
password = ''
rememberMe = true
loading = false
responseStatus = null
async submit() {
try {
this.loading = true;
this.responseStatus = null;
const response = await client.post(new Authenticate({
provider: 'credentials',
userName: this.userName,
password: this.password,
rememberMe: this.rememberMe,
}));
bus.$emit('signin', response);
redirect(this.$props.redirect);
} catch (e) {
this.responseStatus = e.responseStatus || e;
} finally {
this.loading = false;
}
}
switchUser(email:string) {
this.userName = email;
this.password = 'p@55wOrd';
}
}
view raw login.ts hosted with ❤ by GitHub

Source Code and References

<div id="page"></div>
{{#raw appendTo scripts}}
<script src="login.js"></script>
<script>
ReactDOM.render(React.createElement(require('./shared').StateProvider, null,
React.createElement(SignIn, null)), document.getElementById('page'));
</script>
{{/raw}}
view raw login.html hosted with ❤ by GitHub
import * as React from 'react';
import * as classNames from "classnames";
import { useState, useContext } from 'react';
import { StateContext, client, Authenticate, queryString, redirect } from './shared'
import { ErrorSummary, Input, CheckBox } from '@servicestack/react';
const SignIn : React.FC<any> = () => {
const {state, dispatch} = useContext(StateContext);
const [loading, setLoading] = useState(false);
const [responseStatus, setResponseStatus] = useState(null);
const [userName, setUserName] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(true);
const switchUser = (email:string) => {
setUserName(email);
setPassword('p@55wOrd');
};
const submit = async () => {
try {
setLoading(true);
setResponseStatus(null);
const response = await client.post(new Authenticate({
provider: 'credentials',
userName,
password,
rememberMe,
}));
dispatch({ type:'signin', data:response });
redirect(queryString(location.search)['redirect']);
} catch (e) {
setResponseStatus(e.responseStatus || e);
} finally {
setLoading(false);
}
};
return (<div className="col-lg-4">
<h3>Sign In</h3>
<form className={classNames({error:responseStatus, loading})}
onSubmit={async e => { e.preventDefault(); await submit(); }}>
<div className="form-group">
<ErrorSummary responseStatus={responseStatus} except={'userName,password'} />
</div>
<div className="form-group">
<Input type="text" id="userName" value={userName} onChange={setUserName} responseStatus={responseStatus} placeholder="UserName"
label="Email" help="Email you signed up with" />
</div>
<div className="form-group">
<Input type="password" id="password" value={password} onChange={setPassword} responseStatus={responseStatus} placeholder="Password"
label="Password" help="6 characters or more" />
</div>
<div className="form-group">
<CheckBox id="rememberMe" value={rememberMe} onChange={setRememberMe} responseStatus={responseStatus}>
Remember Me
</CheckBox>
</div>
<div className="form-group">
<button type="submit" className="btn btn-lg btn-primary">Login</button>
</div>
<div className="form-group">
<a className="btn btn-outline-primary" href="/register">Register New User</a>
</div>
</form>
<div className="pt-3">
<b>Quick Login:</b>
<p className="pt-1">
<a className="btn btn-outline-info btn-sm" href="javascript:void(0)" onClick={() => switchUser('admin@email.com')}>admin@email.com</a>
{" "}
<a className="btn btn-outline-info btn-sm" href="javascript:void(0)" onClick={() => switchUser('new@user.com')}>new@user.com</a>
</p>
</div>
</div>);
};
view raw login.tsx hosted with ❤ by GitHub

About Client Implementations

Vuetify is a Vue App which uses the popular Vuetify Material Design UI which is in contrast to all other UIs which use Bootstrap. It also uses the JsonServiceClient to send a JSON Authenticate Request whereas all other UIs sends HTML Form x-www-form-urlencoded Key/Value Pairs.

Client TypeScript only needs to render the initial Bootstrap Form Markup as bootstrapForm() takes care of submitting the Ajax Request and binding any validation errors to the form. The data-validation-summary placeholder is used to render any other error summary messages except for the userName or password fields.

Client jQuery uses the exact same markup but uses $('form').bootstrapForm() jQuery plugin to handle the form Ajax request and any error binding.

Client Razor adopts the same jQuery implementation but is rendered using MVC Razor instead of Sharp Pages.

Contacts Page

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:

Server UIs

Source Code and References

{{ `/server/login?continue=${PathInfo}`
| redirectIfNotAuthenticated }}
<div style="text-align:right">
<small class="text-muted">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/server/">Sign Out</a>
</small>
</div>
{{ 'requires-auth' | partial }}
<h3>Add new Contact</h3>
<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>
{{ sendToGateway('GetContacts') | assignTo: response }}
<table id="results">
<tbody>
{{#each response.Results}}
<tr style="background:{{Color}}">
<td>{{Title}} {{Name}} ({{Age}})</td>
<td><a href="/server/contacts/{{Id}}/edit">edit</a></td>
<td><form method="post" action="/contacts/{{Id}}/delete" onsubmit="return confirm('Are you sure?')">
<button class="btn btn-sm btn-primary">delete</button></form></td>
</tr>
{{else}}
<tr>
<td>There are no contacts.</td>
</tr>
{{/each}}
</tbody>
</table>
{{ htmlError }}
view raw index.html hosted with ❤ by GitHub

Source Code and References

{{ `/server-ts/login?continue=${PathInfo}`
| redirectIfNotAuthenticated }}
<div style="text-align:right">
<small class="text-muted">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/server/">Sign Out</a>
</small>
</div>
// generated with https://docs.servicestack.net/typescript-add-servicestack-reference
export class Contact
{
public constructor(init?:Partial<Contact>) { (<any>Object).assign(this, init); }
public id: number;
public userAuthId: number;
public title: Title;
public name: string;
public color: string;
public filmGenres: FilmGenres[];
public age: number;
}
// @Route("/contacts/{Id}", "DELETE")
// @Route("/contacts/{Id}/delete", "POST")
export class DeleteContact implements IReturnVoid
{
public constructor(init?:Partial<DeleteContact>) { (<any>Object).assign(this, init); }
public id: number;
public continue: string;
public createResponse() {}
public getTypeName() { return 'DeleteContact'; }
}
// @Route("/contacts", "GET")
export class GetContacts implements IReturn<GetContactsResponse>
{
public constructor(init?:Partial<GetContacts>) { (<any>Object).assign(this, init); }
public createResponse() { return new GetContactsResponse(); }
public getTypeName() { return 'GetContacts'; }
}
view raw dtos.ts hosted with ❤ by GitHub
{{ 'requires-auth' | partial }}
{{ '/server-ts/contacts/' | assignTo: continue }}
<h3>Add new Contact</h3>
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group">
{{ '<div class="alert alert-danger">{0}</div>' | htmlFormat(errorResponseExcept('title,name,color,filmGenres,age,agree')) }}
<input type="hidden" name="Continue" value="{{ continue }}">
<input type="hidden" name="ErrorView" value="{{ continue }}">
</div>
<div class="form-group">
<div class="form-check" data-invalid="{{ 'title' | errorResponse }}">
{{#each contactTitles}}
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="title-{{Key}}" name="title" value="{{Key}}" class="custom-control-input"
{{ {checked:formValue('title')==Key} | htmlAttrs }}>
<label class="custom-control-label" for="title-{{Key}}">{{Value}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input class="form-control" id="name" name="name" type="text" placeholder="Name"
value="{{ 'name' | formValue }}" data-invalid="{{errorResponse('name')}}">
<small id="name-help" class="text-muted">Your first and last name</small>
</div>
<div class="form-group">
<label class="form-label" for="color">Favorite color</label>
<select id="color" name="color" class="col-4 form-control" data-invalid="{{ 'color' | errorResponse }}">
<option value=""></option>
{{#each contactColors}}
<option value="{{Key}}" {{ {selected:formValue('color')==Key} | htmlAttrs }}>{{Value}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label class="form-check-label">Favorite Film Genres</label>
<div class="form-check" data-invalid="{{ 'filmGenres' | errorResponse }}">
{{ 'filmGenres' | formValues | assignTo: selectedGenres }}
{{#each contactGenres}}
<div class="custom-control custom-checkbox">
<input type="checkbox" id="filmGenres-{{it}}" name="filmGenres" value="{{it}}" class="form-check-input"
{{ {checked:contains(selectedGenres, it)} | htmlAttrs }}>
<label class="form-check-label" for="filmGenres-{{it}}">{{it}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<input class="form-control col-3" name="age" type="number" min="13" placeholder="Age"
value="{{ 'age' | formValue }}" data-invalid="{{errorResponse('age')}}">
</div>
<div class="form-group">
<div class="form-check">
<input class=" form-check-input" id="agree" name="agree" type="checkbox" value="true"
data-invalid="{{ 'agree' | errorResponse }}">
<label class="form-check-label" for="agree">Agree to terms and conditions</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="{{ continue }}">reset</a>
</div>
</form>
<table id="results"></table>
{{#capture appendTo scripts}}
<script>var CONTACTS = {{ sendToGateway('GetContacts') | map => it.Results | json }};</script>
{{/capture}}
{{#raw appendTo scripts}}
<script src="/server-ts/contacts/index.js"></script>
{{/raw}}
{{ htmlError }}
view raw index.html hosted with ❤ by GitHub
import {bindHandlers, bootstrap, JsonServiceClient} from "@servicestack/client";
import {Contact, DeleteContact, GetContacts} from "../../../dtos";
declare var CONTACTS:Contact[];
const client = new JsonServiceClient();
bootstrap(); //converts data-invalid attributes into Bootstrap v4 error messages.
bindHandlers({
deleteContact: async function(id:number) {
if (!confirm('Are you sure?'))
return;
await client.delete(new DeleteContact({ id }));
const response = await client.get(new GetContacts());
CONTACTS = response.results;
render();
}
});
const contactRow = (contact:Contact) =>
`<tr style="background:${contact.color}">
<td>${contact.title} ${contact.name} (${contact.age})</td>
<td><a href="/server-ts/contacts/${contact.id}/edit">edit</a></td>
<td><button class="btn btn-sm btn-primary" data-click="deleteContact:${contact.id}">delete</button></td>
</tr>`;
function render() {
let sb = "";
if (CONTACTS.length > 0) {
for (let i=0; i<CONTACTS.length; i++) {
sb += contactRow(CONTACTS[i])
}
} else {
sb = "<tr><td>There are no contacts.</td></tr>";
}
document.querySelector("#results")!.innerHTML = `<tbody>${sb}</tbody>`;
}
render();
view raw index.ts hosted with ❤ by GitHub

Source Code and References

{{ `/server-jquery/login?continue=${PathInfo}`
| redirectIfNotAuthenticated }}
<div style="text-align:right">
<small class="text-muted">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/server/">Sign Out</a>
</small>
</div>
{{ 'requires-auth' | partial }}
{{ '/server-jquery/contacts/' | assignTo: continue }}
<h3>Add new Contact</h3>
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group">
{{ '<div class="alert alert-danger">{0}</div>' | htmlFormat(errorResponseExcept('title,name,color,filmGenres,age,agree')) }}
<input type="hidden" name="Continue" value="{{ continue }}">
<input type="hidden" name="ErrorView" value="{{ continue }}">
</div>
<div class="form-group">
<div class="form-check" data-invalid="{{ 'title' | errorResponse }}">
{{#each contactTitles}}
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="title-{{Key}}" name="title" value="{{Key}}" class="custom-control-input"
{{ {checked:formValue('title')==Key} | htmlAttrs }}>
<label class="custom-control-label" for="title-{{Key}}">{{Value}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input class="form-control" id="name" name="name" type="text" placeholder="Name"
value="{{ 'name' | formValue }}" data-invalid="{{errorResponse('name')}}">
<small id="name-help" class="text-muted">Your first and last name</small>
</div>
<div class="form-group">
<label class="form-label" for="color">Favorite color</label>
<select id="color" name="color" class="col-4 form-control" data-invalid="{{ 'color' | errorResponse }}">
<option value=""></option>
{{#each contactColors}}
<option value="{{Key}}" {{ {selected:formValue('color')==Key} | htmlAttrs }}>{{Value}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label class="form-check-label">Favorite Film Genres</label>
<div class="form-check" data-invalid="{{ 'filmGenres' | errorResponse }}">
{{ 'filmGenres' | formValues | assignTo: selectedGenres }}
{{#each contactGenres}}
<div class="custom-control custom-checkbox">
<input type="checkbox" id="filmGenres-{{it}}" name="filmGenres" value="{{it}}" class="form-check-input"
{{ {checked:contains(selectedGenres, it)} | htmlAttrs }}>
<label class="form-check-label" for="filmGenres-{{it}}">{{it}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<input class="form-control col-3" name="age" type="number" min="13" placeholder="Age"
value="{{ 'age' | formValue }}" data-invalid="{{errorResponse('age')}}">
</div>
<div class="form-group">
<div class="form-check">
<input class=" form-check-input" id="agree" name="agree" type="checkbox" value="true"
data-invalid="{{ 'agree' | errorResponse }}">
<label class="form-check-label" for="agree">Agree to terms and conditions</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="{{ continue }}">reset</a>
</div>
</form>
<table id="results"></table>
{{#capture appendTo scripts}}
<script>var CONTACTS = {{ sendToGateway('GetContacts') | map => it.Results | json }};</script>
{{/capture}}
{{#raw appendTo scripts}}
<script>
$(document).bootstrap().bindHandlers({
deleteContact: function(id) {
if (!confirm('Are you sure?'))
return;
$.post("/contacts/" + id + "/delete", function(r) {
$.getJSON("/contacts", function(r) {
CONTACTS = r.results;
render();
})
});
}
});
function contactRow(contact) {
return '<tr style="background:' + contact.color + '">' +
'<td>' + contact.title + ' ' + contact.name + ' (' + contact.age + ')</td>' +
'<td><a href="/server-jquery/contacts/' + contact.id + '/edit">edit</a></td>' +
'<td><button class="btn btn-sm btn-primary" data-click="deleteContact:' + contact.id + '">delete</button></td>' +
'</tr>';
}
function render() {
var sb = "";
if (CONTACTS.length > 0) {
for (var i=0; i<CONTACTS.length; i++) {
sb += contactRow(CONTACTS[i])
}
} else {
sb = "<tr><td>There are no contacts.</td></tr>";
}
$("#results").html("<tbody>" + sb + "</tbody>");
}
render();
</script>
{{/raw}}
{{ htmlError }}
view raw index.html hosted with ❤ by GitHub

Source Code and References

@{ RedirectIfNotAuthenticated($"/server-razor/login?continue={Request.PathInfo}"); }
<div style="text-align:right">
<small class="text-muted">
@UserSession.DisplayName
| <a href="/auth/logout?continue=/server-razor/">Sign Out</a>
</small>
</div>
@await Html.PartialAsync("_RequiresAuthServer")
@{ var Continue = "/server-razor/contacts/"; }
<h3>Add new Contact</h3>
<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>
@{ var response = await Gateway.SendAsync(new GetContacts()); }
<table id="results">
<tbody>
@foreach (var c in response.Results)
{
<tr style="background:@c.Color">
<td>@c.Title @c.Name (@c.Age)</td>
<td><a href="/server-razor/contacts/@c.Id/edit">edit</a></td>
<td><form method="post" action="/contacts/@c.Id/delete" onsubmit="return confirm('Are you sure?')">
<input type="hidden" name="continue" value="@Continue">
<button class="btn btn-sm btn-primary">delete</button></form></td>
</tr>
}
@if (response.Results.IsEmpty())
{
<tr>
<td>There are no contacts.</td>
</tr>
}
</tbody>
</table>
view raw default.cshtml hosted with ❤ by GitHub

About Server Implementations

Both the Contacts UIs and Contacts Services are protected resources which uses a partial to protect its pages. Normally redirectIfNotAuthenticated wouldn’t require a URL, but one is needed here so it knows the right login page it should redirect to.

Sharp Pages

In Sharp Pages our wrist-friendly server controls are back as we start to see more of its features. The arguments of the left of the formInput are for HTML attributes you want rendered on the input control whilst the arguments on the right (or 2nd argument) are to enlist the controls other “high-level features” like values which is used to populate a list of radio and checkboxes or formSelect options. The inline argument tells the control to render multiple controls in-line whilst you can use help to render some help text as an aside.

We also see the introduction of the sendToGateway method used to send the GetContacts Request DTO to call its Service using the Service Gateway, the Response of which is used to render the list of contacts on the Server.

Another difference is that there are multiple <form> elements on this page to handle deleting a contact by submitting an empty form post to /contacts//delete.

Sharp Pages doesn’t need to specify its own ErrorView or Continue Request params as its the default view used for ContactServices:

[DefaultView("/server/contacts")] // Render custom HTML View for HTML Requests
public class ContactServices : Service { ... }

This is typically all that’s needed, as most real-world Apps would rarely have more than 1 HTML View per Service.

Server TypeScript

With Server TypeScript you’re starting to see the additional effort required when you need to use your own custom markup to render form controls.

It differs with Sharp Pages in that instead of rendering the list of contacts on the server, it renders the GetContacts Response DTO as JSON which is interpreted in the browser as a native JS Object literal which the render() method uses to render the list of contacts in the browser.

Deleting a contact is also handled differently where it uses the JsonServiceClient to send the DeleteContact Request DTO from the generated dtos.ts. After the request completes it uses GetContacts to fetch an updated list of Contacts which it re-renders.

Server jQuery

Server jQuery adopts the same approach as Server TypeScript but renders it using jQuery and uses custom routes constructed on the client with jQuery’s Ajax APIs to call the ContactServices.

Server Razor

Server Razor is very similar to Sharp Pages but implemented using Razor. In many cases the built-in script methods in Sharp Pages have Razor equivalents, either in the base ViewPage<T> class like RedirectIfNotAuthenticated() or as a @Html helper.

Client UIs

Source Code and References

{{ 'requires-auth' | partial }}
{{#raw}}
<template>
<v-layout justify-center>
<v-flex xs6>
<v-toolbar color="pink">
<v-toolbar-title class="white--text">
{{ heading }}
</v-toolbar-title>
</v-toolbar>
<v-card>
<v-form v-model="valid" ref="form" lazy-validation @keyup.native.enter="submit">
<v-container>
<v-alert outline color="error" icon="warning" :value="errorSummary">{{ errorSummary }}</v-alert>
<v-layout column>
<v-radio-group
v-model="title"
:error-messages="errorResponse('title')"
row
><v-radio
v-for="x in contactTitles"
:key="x.key"
:label="x.value"
:value="x.key"
></v-radio>
</v-radio-group>
<v-text-field
label="Name"
v-model="name"
required
:rules="nameRules"
:error-messages="errorResponse('name')"
></v-text-field>
<v-select v-model="color"
:items="contactColors"
label="Favorite color"
:error-messages="errorResponse('color')"
solo
></v-select>
<v-radio-group class="mb-3" :error-messages="errorResponse('filmGenres')">
<v-checkbox
v-for="x in contactGenres"
v-model="filmGenres"
:key="x"
:label="x"
:value="x"
></v-checkbox>
</v-radio-group>
<v-layout>
<v-flex xs4>
<v-text-field
v-model="age"
class="mt-0"
label="Age"
type="number"
:error-messages="errorResponse('age')"
></v-text-field>
</v-flex>
</v-layout>
<v-checkbox v-if="!update"
label="Agree to terms and conditions"
v-model="agree"
:value="true"
:rules="[v => v || 'You must agree to our terms']"
:error-messages="errorResponse('agree')"
></v-checkbox>
</v-layout>
</v-container>
</v-form>
<v-card-actions>
<v-flex>
<v-btn large class="mb-2" color="primary" @click="submit" :disabled="!valid || loading">
{{ action }}
</v-btn>
<v-btn flat @click="reset">reset</v-btn>
<v-btn v-if="update" flat @click="cancel">cancel</v-btn>
</v-flex>
</v-card-actions>
</v-card>
<v-flex>
<v-toolbar>
<v-toolbar-title>
Contacts
</v-toolbar-title>
</v-toolbar>
<v-card>
<v-layout v-for="c in contacts" :key="c.id" :style="`background:${c.color}`">
<v-flex>
<h3 class="pa-3 title">{{c.title}} {{c.name}} ({{c.age}})</h3>
</v-flex>
<v-btn flat @click="edit(c.id)">edit</v-btn>
<v-btn @click="remove(c.id)">delete</v-btn>
</v-layout>
<v-layout v-if="!contacts.length">
<v-flex>
<h3 class="pa-3 title">There are no contacts.</h3>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<div class="src">
<h4>Source Code and References</h4>
<div><a href="https://github.com/NetCoreApps/Validation/blob/master/world/wwwroot/vuetify/_layout.html" class="ref">/vuetify/_layout.html</a></div>
<div><a href="https://github.com/NetCoreApps/Validation/blob/master/world/wwwroot/vuetify/contacts/index.html" class="ref">/vuetify/contacts/index.html</a></div>
<div><a href="https://github.com/NetCoreApps/Validation/blob/master/world/wwwroot/vuetify/contacts/index.ts" class="ref">/vuetify/contacts/index.ts</a></div>
<div><a href="https://github.com/NetCoreApps/Validation/blob/master/world/wwwroot/dtos.ts" class="ref">/dtos.ts</a></div>
</div>
</v-flex>
</v-layout>
</template>
{{/raw}}
{{#capture appendTo scripts}}
<script>
var colors = {{ contactColors | json }};
var DATA = {
contacts: {{ sendToGateway('GetContacts') | map => it.Results | json }},
contactTitles: {{ contactTitles | json }},
contactColors: Object.keys(colors).map(k => ({ text:colors[k], value:k })),
contactGenres: {{ contactGenres | json }}
};
</script>
<script src="index.js"></script>
{{/capture}}
view raw index.html hosted with ❤ by GitHub
import { Vue } from 'vue';
import { errorResponse, errorResponseExcept, queryString } from '@servicestack/client';
import { client, nameRules, } from '../shared';
import {CreateContact, DeleteContact, GetContact, GetContacts, Title, UpdateContact} from '../../dtos';
declare var DATA:any;
new Vue({
el: '#app',
computed: {
heading: function() {
return this.update ? 'Edit new Contact' : 'Add new Contact';
},
action: function() {
return this.update? 'Update Contact' : 'Add Contact';
},
errorSummary: function() {
return errorResponseExcept.call(this, 'title,name,color,filmGenres,age,agree');
},
},
methods: {
async submit() {
const form = (this.$refs.form as HTMLFormElement);
if (form.validate()) {
try {
this.loading = true;
const request = {
title: this.title as Title,
name: this.name,
color: this.color,
filmGenres: this.filmGenres,
age: this.age,
};
if (this.update) {
await client.post(new UpdateContact({...request, id: this.id }));
} else {
await client.post(new CreateContact({...request, agree: this.agree }));
}
this.update = false;
this.responseStatus = null;
form.reset();
} catch (e) {
this.responseStatus = e.responseStatus || e;
} finally {
this.loading = false;
form.resetValidation();
}
await this.refresh();
}
},
async refresh() {
this.contacts = (await client.get(new GetContacts())).results;
},
reset() {
(this.$refs.form as HTMLFormElement).reset();
},
cancel() {
this.reset();
this.update = false;
},
async edit(id:number) {
this.update = true;
const contact = (await client.get(new GetContact({ id }))).result;
Object.assign(this, contact);
},
async remove(id:number) {
if (!confirm('Are you sure?'))
return;
await client.delete(new DeleteContact({ id }));
const response = await client.get(new GetContacts());
await this.refresh();
},
errorResponse
},
async mounted() {
const qs = queryString(location.href);
if (qs['id'])
await this.edit(parseInt(qs['id']))
},
data: () => ({
loading: false,
valid: true,
update: false,
...DATA,
id:0,
title: "",
name: "",
color: "",
filmGenres: [],
age: 13,
agree: false,
nameRules,
responseStatus: null
}),
});
view raw index.ts hosted with ❤ by GitHub

Source Code and References

{{ `/client-ts/login?continue=${PathInfo}`
| redirectIfNotAuthenticated }}
<div style="text-align:right">
<small class="text-muted">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/client-ts/">Sign Out</a>
</small>
</div>
{{ 'requires-auth' | partial }}
<h3>Add new Contact</h3>
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group" data-validation-summary="title,name,color,filmGenres,age,agree"></div>
<div class="form-group">
<div class="form-check">
{{#each contactTitles}}
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="title-{{Key}}" name="title" value="{{Key}}" class="custom-control-input">
<label class="custom-control-label" for="title-{{Key}}">{{Value}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input class="form-control" id="name" name="name" type="text" placeholder="Name">
<small id="name-help" class="text-muted">Your first and last name</small>
</div>
<div class="form-group">
<label class="form-label" for="color">Favorite color</label>
<select id="color" name="color" class="col-4 form-control">
<option value=""></option>
{{#each contactColors}}
<option value="{{Key}}">{{Value}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label class="form-check-label">Favorite Film Genres</label>
<div class="form-check">
{{#each contactGenres}}
<div class="custom-control custom-checkbox">
<input type="checkbox" id="filmGenres-{{it}}" name="filmGenres" value="{{it}}" class="form-check-input">
<label class="form-check-label" for="filmGenres-{{it}}">{{it}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<input class="form-control col-3" name="age" type="number" min="13" placeholder="Age">
</div>
<div class="form-group">
<div class="form-check">
<input class=" form-check-input" id="agree" name="agree" type="checkbox" value="true">
<label class="form-check-label" for="agree">Agree to terms and conditions</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="/client-ts/contacts/">reset</a>
</div>
</form>
<table id="results"></table>
{{#capture appendTo scripts}}
<script>var CONTACTS = {{ sendToGateway('GetContacts') | map => it.Results | json }};</script>
{{/capture}}
{{#raw appendTo scripts}}
<script src="index.js"></script>
{{/raw}}
{{ htmlError }}
view raw index.html hosted with ❤ by GitHub
import { bootstrapForm, bindHandlers, JsonServiceClient } from "@servicestack/client";
import {Contact, DeleteContact, GetContact, GetContacts} from "../../../dtos";
declare var CONTACTS:Contact[];
const client = new JsonServiceClient();
const form = document.querySelector("form")!;
bootstrapForm(form,{
success: function (r:{result:Contact}) {
form.reset();
CONTACTS.push(r.result);
render();
}
});
bindHandlers({
deleteContact: async function(id:number) {
if (!confirm('Are you sure?'))
return;
await client.delete(new DeleteContact({ id }));
const response = await client.get(new GetContacts());
CONTACTS = response.results;
render();
}
});
const contactRow = (contact:Contact) =>
`<tr style="background:${contact.color}">
<td>${contact.title} ${contact.name} (${contact.age})</td>
<td><a href="/client-ts/contacts/${contact.id}/edit">edit</a></td>
<td><button class="btn btn-sm btn-primary" data-click="deleteContact:${contact.id}">delete</button></td>
</tr>`;
function render() {
let sb = "";
if (CONTACTS.length > 0) {
for (let i=0; i<CONTACTS.length; i++) {
sb += contactRow(CONTACTS[i])
}
} else {
sb = "<tr><td>There are no contacts.</td></tr>";
}
document.querySelector("#results")!.innerHTML = `<tbody>${sb}</tbody>`;
}
render();
view raw index.ts hosted with ❤ by GitHub

Source Code and References

{{ `/client-jquery/login?continue=${PathInfo}`
| redirectIfNotAuthenticated }}
<div style="text-align:right">
<small class="text-muted">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/client-jquery/">Sign Out</a>
</small>
</div>
{{ 'requires-auth' | partial }}
<h3>Add new Contact</h3>
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group" data-validation-summary="title,name,color,filmGenres,age,agree"></div>
<div class="form-group">
<div class="form-check">
{{#each contactTitles}}
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="title-{{Key}}" name="title" value="{{Key}}" class="custom-control-input">
<label class="custom-control-label" for="title-{{Key}}">{{Value}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input class="form-control" id="name" name="name" type="text" placeholder="Name">
<small id="name-help" class="text-muted">Your first and last name</small>
</div>
<div class="form-group">
<label class="form-label" for="color">Favorite color</label>
<select id="color" name="color" class="col-4 form-control">
<option value=""></option>
{{#each contactColors}}
<option value="{{Key}}">{{Value}}</option>
{{/each}}
</select>
</div>
<div class="form-group">
<label class="form-check-label">Favorite Film Genres</label>
<div class="form-check">
{{#each contactGenres}}
<div class="custom-control custom-checkbox">
<input type="checkbox" id="filmGenres-{{it}}" name="filmGenres" value="{{it}}" class="form-check-input">
<label class="form-check-label" for="filmGenres-{{it}}">{{it}}</label>
</div>
{{/each}}
</div>
</div>
<div class="form-group">
<input class="form-control col-3" name="age" type="number" min="13" placeholder="Age">
</div>
<div class="form-group">
<div class="form-check">
<input class=" form-check-input" id="agree" name="agree" type="checkbox" value="true">
<label class="form-check-label" for="agree">Agree to terms and conditions</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="/client-jquery/contacts/">reset</a>
</div>
</form>
<table id="results"></table>
{{#capture appendTo scripts}}
<script>var CONTACTS = {{ sendToGateway('GetContacts') | map => it.Results | json }};</script>
{{/capture}}
{{#raw appendTo scripts}}
<script>
$("form").bootstrapForm({
success: function (r) {
$("form")[0].reset();
CONTACTS.push(r.result);
render();
}
});
$(document).bindHandlers({
deleteContact: function(id) {
if (!confirm('Are you sure?'))
return;
$.post("/contacts/" + id + "/delete", function(r) {
$.getJSON("/contacts", function(r) {
CONTACTS = r.results;
render();
})
});
}
});
function contactRow(contact) {
return '<tr style="background:' + contact.color + '">' +
'<td>' + contact.title + ' ' + contact.name + ' (' + contact.age + ')</td>' +
'<td><a href="/client-jquery/contacts/' + contact.id + '/edit">edit</a></td>' +
'<td><button class="btn btn-sm btn-primary" data-click="deleteContact:' + contact.id + '">delete</button></td>' +
'</tr>';
}
function render() {
var sb = "";
if (CONTACTS.length > 0) {
for (var i=0; i<CONTACTS.length; i++) {
sb += contactRow(CONTACTS[i])
}
} else {
sb = "<tr><td>There are no contacts.</td></tr>";
}
$("#results").html("<tbody>" + sb + "</tbody>");
}
render();
</script>
{{/raw}}
{{ htmlError }}
view raw index.html hosted with ❤ by GitHub

Source Code and References

@{ RedirectIfNotAuthenticated($"/client-razor/login?continue={Request.PathInfo}"); }
<div style="text-align:right">
<small class="text-muted">
@UserSession.DisplayName
| <a href="/auth/logout?continue=/client-razor/">Sign Out</a>
</small>
</div>
view raw _RequiresAuth.cshtml hosted with ❤ by GitHub
@await Html.PartialAsync("_RequiresAuth")
<h3>Add new Contact</h3>
<form action="/contacts" method="post" class="col-lg-4">
<div class="form-group" data-validation-summary="title,name,color,filmGenres,age,agree"></div>
<div class="form-group">
<div class="form-check">
@foreach (var it in Html.ContactTitles())
{
<div class="custom-control custom-radio custom-control-inline">
<input type="radio" id="title-@it.Key" name="title" value="@it.Key" class="custom-control-input">
<label class="custom-control-label" for="title-@it.Key">@it.Value</label>
</div>
}
</div>
</div>
<div class="form-group">
<label for="name">Full Name</label>
<input class="form-control" id="name" name="name" type="text" placeholder="Name">
<small id="name-help" class="text-muted">Your first and last name</small>
</div>
<div class="form-group">
<label class="form-label" for="color">Favorite color</label>
<select id="color" name="color" class="col-4 form-control">
<option value=""></option>
@foreach (var it in Html.ContactColors())
{
<option value="@it.Key">@it.Value</option>
}
</select>
</div>
<div class="form-group">
<label class="form-check-label">Favorite Film Genres</label>
<div class="form-check">
@foreach (var it in Html.ContactGenres())
{
<div class="custom-control custom-checkbox">
<input type="checkbox" id="filmGenres-@it" name="filmGenres" value="@it" class="form-check-input">
<label class="form-check-label" for="filmGenres-@it">@it</label>
</div>
}
</div>
</div>
<div class="form-group">
<input class="form-control col-3" name="age" type="number" min="13" placeholder="Age">
</div>
<div class="form-group">
<div class="form-check">
<input class=" form-check-input" id="agree" name="agree" type="checkbox" value="true">
<label class="form-check-label" for="agree">Agree to terms and conditions</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="/client-razor/contacts/">reset</a>
</div>
</form>
<table id="results"></table>
@section scripts
{
<script>
var CONTACTS = @((await Gateway.SendAsync(new GetContacts())).Results.AsRawJson());
$("form").bootstrapForm({
success: function (r) {
$("form")[0].reset();
CONTACTS.push(r.result);
render();
}
});
$(document).bindHandlers({
deleteContact: function(id) {
if (!confirm('Are you sure?'))
return;
$.post("/contacts/" + id + "/delete", function(r) {
$.getJSON("/contacts", function(r) {
CONTACTS = r.results;
render();
})
});
}
});
function contactRow(contact) {
return '<tr style="background:' + contact.color + '">' +
'<td>' + contact.title + ' ' + contact.name + ' (' + contact.age + ')</td>' +
'<td><a href="/client-razor/contacts/' + contact.id + '/edit">edit</a></td>' +
'<td><button class="btn btn-sm btn-primary" data-click="deleteContact:' + contact.id + '">delete</button></td>' +
'</tr>';
}
function render() {
var sb = "";
if (CONTACTS.length > 0) {
for (var i=0; i<CONTACTS.length; i++) {
sb += contactRow(CONTACTS[i])
}
} else {
sb = "<tr><td>There are no contacts.</td></tr>";
}
$("#results").html("<tbody>" + sb + "</tbody>");
}
render();
</script>
}
view raw default.cshtml hosted with ❤ by GitHub

Source Code and References

{{ `/client-vue/login?redirect=${PathInfo}`
| redirectIfNotAuthenticated }}
<div id="auth-info">
<small class="body-2">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/client-vue/">Sign Out</a>
</small>
</div>
{{ 'requires-auth' | partial }}
<div id="page"></div>
{{#capture appendTo scripts}}
<script src="index.js"></script>
<script>
var colors = {{ contactColors | json }};
new Contacts({
el: '#page',
data: {
contacts: {{ sendToGateway('GetContacts') | map => it.Results | json }},
contactTitles: {{ contactTitles | json }},
contactColors: Object.keys(colors).map(k => ({ key:k, value:colors[k] })),
contactGenres: {{ contactGenres | json }}
}
})
</script>
{{/capture}}
view raw index.html hosted with ❤ by GitHub
import { Vue, Component } from 'vue-property-decorator';
import { client } from '../shared';
import { CreateContact, DeleteContact, GetContact, GetContacts, Contact, Title } from '../../dtos';
@Component({ template:
`<div>
<h3>Add new Contact</h3>
<form ref="form" @submit.prevent="submit" @keyup.native.enter="submit">
<ErrorSummary :responseStatus="responseStatus" except="title,name,color,filmGenres,age,agree" />
<div class="form-group">
<Input type="radio" id="title" v-model="title" :values="contactTitles" :inline="true" :responseStatus="responseStatus" />
</div>
<div class="form-group">
<Input id="name" v-model="name" placeholder="Name" :responseStatus="responseStatus"
label="Full Name" help="Your first and last name" />
</div>
<div class="form-group">
<Select id="color" v-model="color" :values="['',...contactColors]" label="Favorite color" :responseStatus="responseStatus" />
</div>
<div class="form-group">
<Input type="checkbox" id="filmGenres" v-model="filmGenres" :values="contactGenres"
label="Favorite Film Genres" help="choose one or more" :responseStatus="responseStatus" />
</div>
<div class="form-group">
<Input type="number" id="age" v-model="age" inputClass="col-4" placeholder="Age" :responseStatus="responseStatus" />
</div>
<div class="form-group">
<CheckBox name="agree" v-model="agree" :responseStatus="responseStatus">
Agree to terms and conditions
</CheckBox>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Add Contact</button>
<a href="javascript:void(0)" @click.prevent="reset">reset</a>
</div>
</form>
<table id="results">
<tr v-for="c in results" :key="c.id" :style="concat('background:',c.color)">
<td>{{c.title}} {{c.name}} ({{c.age}})</td>
<td><a :href="concat('/client-vue/contacts/',c.id,'/edit')">edit</a></td>
<td><button class="btn btn-sm btn-primary" @click="remove(c.id)">delete</button></td>
</tr>
</table>
</div>`,
props: {
redirect: String
}
})
export class Contacts extends Vue {
loading = false;
valid = true;
title = "";
name = "";
color = "";
filmGenres = [];
age = 13;
agree = false;
results:Contact[] = [];
responseStatus = null;
async mounted() {
this.results = (this as any).contacts;
}
concat(prefix:string,id:string,suffix:string) {
return prefix + id + (suffix || '');
}
async submit() {
try {
this.loading = true;
const request = new CreateContact({
title: this.title as Title,
name: this.name,
color: this.color,
filmGenres: this.filmGenres,
age: this.age,
agree: this.agree
});
await client.post(request);
await this.refresh();
this.responseStatus = null;
this.reset();
} catch (e) {
this.responseStatus = e.responseStatus || e;
} finally {
this.loading = false;
}
}
async refresh() {
this.results = (await client.get(new GetContacts())).results;
}
reset() {
this.title = "";
this.name = "";
this.color = "";
this.filmGenres = [];
this.age = 13;
this.agree = false;
this.responseStatus = null;
}
cancel() {
this.reset();
}
async remove(id:number) {
if (!confirm('Are you sure?'))
return;
await client.delete(new DeleteContact({ id }));
await this.refresh();
}
}
view raw index.ts hosted with ❤ by GitHub

Source Code and References

{{ `/client-react/login?redirect=${PathInfo}`
| redirectIfNotAuthenticated }}
<div id="auth-info">
<small class="body-2">
{{ userSession.DisplayName }}
| <a href="/auth/logout?continue=/client-react/">Sign Out</a>
</small>
</div>
{{ 'requires-auth' | partial }}
<div id="page"></div>
{{#capture appendTo scripts}}
<script src="index.js"></script>
<script>
var colors = {{ contactColors | json }};
ReactDOM.render(React.createElement(require('./shared').StateProvider, null,
React.createElement(Contacts, {
contacts: {{ sendToGateway('GetContacts') | map => it.Results | json }},
contactTitles: {{ contactTitles | json }},
contactColors: Object.keys(colors).map(k => ({ key:k, value:colors[k] })),
contactGenres: {{ contactGenres | json }}
})), document.getElementById('page'));
</script>
{{/capture}}
view raw index.html hosted with ❤ by GitHub
import * as React from 'react';
import { useState, useContext } from 'react';
import { StateContext, client } from '../shared'
import { ErrorSummary, Input, CheckBox, Select } from '@servicestack/react';
import {Contact, CreateContact, DeleteContact, GetContacts, Title} from "../../dtos";
const Contacts : React.FC<any> = ({ contacts, contactTitles, contactColors, contactGenres }) => {
const {state, dispatch} = useContext(StateContext);
const [loading, setLoading] = useState(false);
const [responseStatus, setResponseStatus] = useState(null);
const [title, setTitle] = useState('');
const [name, setName] = useState('');
const [color, setColor] = useState('');
const [filmGenres, setFilmGenres] = useState([]);
const [age, setAge] = useState(13);
const [agree, setAgree] = useState(false);
const [results, setResults] = useState(contacts as Contact[]);
async function submit() {
try {
setLoading(true);
const request = new CreateContact({
title: title as Title,
name,
color,
filmGenres,
age,
agree
});
await client.post(request);
await refresh();
setResponseStatus(null);
reset();
} catch (e) {
setResponseStatus(e.responseStatus || e);
} finally {
setLoading(false);
}
}
async function refresh() {
setResults((await client.get(new GetContacts())).results);
}
function reset() {
setTitle('');
setName('');
setColor('');
setFilmGenres([]);
setAge(13);
setAgree(false);
}
async function remove(id:number) {
if (!confirm('Are you sure?'))
return;
await client.delete(new DeleteContact({ id }));
await refresh();
}
return (<div className="col-lg-4">
<h3>Add new Contact</h3>
<form onSubmit={async e => { e.preventDefault(); await submit(); }}>
<ErrorSummary except="title,name,color,filmGenres,age,agree" responseStatus={responseStatus} />
<div className="form-group">
<Input type="radio" id="title" value={title} onChange={setTitle} values={contactTitles} inline={true} responseStatus={responseStatus} />
</div>
<div className="form-group">
<Input id="name" value={name} onChange={setName} placeholder="Name" responseStatus={responseStatus}
label="Full Name" help="Your first and last name" />
</div>
<div className="form-group">
<Select id="color" value={color} onChange={setColor} values={['',...contactColors]} label="Favorite color" responseStatus={responseStatus} />
</div>
<div className="form-group">
<Input type="checkbox" id="filmGenres" value={filmGenres} onChange={setFilmGenres} values={contactGenres} responseStatus={responseStatus}
label="Favorite Film Genres" help="choose one or more" />
</div>
<div className="form-group">
<Input type="number" id="age" value={`${age}`} onChange={x => setAge(Number(x))} inputClass="col-4" placeholder="Age" responseStatus={responseStatus} />
</div>
<div className="form-group">
<CheckBox id="agree" value={agree} onChange={setAgree} responseStatus={responseStatus}>
Agree to terms and conditions
</CheckBox>
</div>
<div className="form-group">
<button className="btn btn-primary" type="submit">Add Contact</button>
{" "}
<a href="javascript:void(0)" onClick={e => reset()}>reset</a>
</div>
</form>
<table id="results">
<tbody>
{results.map(c => (
<tr key={c.id} style={ {background:c.color} }>
<td>{c.title} {c.name} ({c.age})</td>
<td><a href={`/client-react/contacts/${c.id}/edit`}>edit</a></td>
<td><button className="btn btn-sm btn-primary" onClick={e => remove(c.id)}>delete</button></td>
</tr>
))}
</tbody>
</table>
</div>);
};
view raw index.tsx hosted with ❤ by GitHub

About Client Implementations

Vuetify ends up being larger than other implementations as it also handles Edit Contacts functionality which is a separate page in other UIs. It also includes additional functionality like client-side validation enabled in each control using its :rules attribute. One thing that remains consistent is the way to call ServiceStack Services and handle errors by assigning it to this.responseStatus which the reactive errorResponse method uses to bind to each control.

The remaining client implementations show that whilst the server controls require the least code, if you need custom markup it’s much easier to render the initial markup once, then use bootstrapForm() to bind any validation errors and handle the ajax form submissions. It’s especially valuable when you need to update a form where the same markup can be populated by just assigning the model property as done in the Edit Contact Pages:

const form = document.querySelector("form")!;
bootstrapForm(form,{
    model: CONTACT,
    success: function () {
        location.href = '/client-ts/contacts/';
    }
});

The amount of code can be even further reduced when using an SPA framework that allows easy componentization as seen in the Vue Form Validation and React Form Validation examples.