ServiceStack v6.7

We're excited to showcase the advancements in our comprehensive Vue 3 Tailwind Components library which is now embedded inside of ServiceStack.dll, paving the way for exciting new modern UI's we can now deliver inside of ServiceStack.

Auto API HTML Page

The first feature to benefit from this feature is the HTML 5 Auto HTML page that's been much loved feature in ServiceStack for 10+ years which provided a HTML fallback page for your APIs when called from a browser (i.e. requesting a text/html Content-Type) which would return a human-friendly snapshot allowing developers to quickly visualize all the data returned by the API Response in a glance:

JSON
HTML

The new Auto HTML Page retains the same objective as the previous Auto HTML page it replaces, but with access to modern JavaScript, full Vue 3 and @servicestack/vue rich component library we can do much better - meet the new Auto HTML API v2!

Auto HTML API v2

The best way to visualize, inspect and integrate with your APIs in an instant!

Where now the fallback HTML page of each API gets its very own API Visualizer, JSON syntax highlighting, integrated Postman UI and API SDK integration all-in-one! Lets take a quick tour through each of these features:

PREVIEW

Calling our APIs in a browser will greet us with the Preview page which uses the HtmlFormat to display the API response in a beautiful Tailwind style with links to different Content-Type Formats and direct links to view it in API Explorer and Locode for AutoQuery APIs:

JSON

Developers who wish to view the actual JSON API Response can click on the JSON tab to view the JSON in pretty-formatted syntax-highlighted form with a 1-click button to copy:

FORM

You don't need to build UIs before non-developers can access your APIs with the FORM tab which uses the new AutoForm component to render an optimal UI to call an API that you can further customize from your C# Request DTOs:

CODE

The CODE tab gives you an appealing API docs page you can immediately share with any 3rd Party Developers that want to consume your APIs, with simple step-by-step instructions for how to call your APIs from their preferred programming language:

A nice benefit of ServiceStack's API Design is that consuming APIs are fundamentally all done the same way in all languages, which just requires adding a dependency containing a generic ServiceClient which can be used to call any ServiceStack API using the typed DTOs copied directly from the API docs page to enable an end-to-end typed API without any external tooling or build steps.

API Fallback HTML Page

The Auto HTML API is the fallback HTML page returned for APIs when calling user-defined routes from a browser (i.e. Accept: text/html):

https://northwind.netcore.io/customers/ALFKI

When calling the /api pre-defined route with the .html extension:

https://northwind.netcore.io/api/GetCustomerDetails.html?Id=ALFKI

When calling the /api pre-defined route with ?format=html:

https://northwind.netcore.io/api/GetCustomerDetails?Id=ALFKI&format=html

AutoQueryGrid

The new AutoQueryGrid component is the last high-productivity component missing from the Vue Tailwind Component Library to reach feature parity with Blazor Tailwind Component Library.

Blazor & Vue AutoQueryGrid side-by-side

Despite being implemented with completely different UI technologies, both AutoQueryGrid components offer identical functionality where either can be used to enable instant CRUD functionality for your AutoQuery Services in either Blazor Tailwind WASM or Server or Vue Tailwind project templates.

Thanks to Vue's progressive enhancement abilities we can easily compare them running side-by-side. In the last v6.6 release we demonstrated the power of JS Modules by loading a Vue DataGrid inside a running Blazor Server App. We can now do a lot more with less, where we can completely replicate Blazor Server AutoQuery Crud functionality with a single <AutoQueryGrid/> component.

For a live demonstration of this in action, head over to the Bookings page and Sign In:

blazor.web-templates.io/secure/bookings

Then press F12 to open the dev console again to paste and run the code below:

const im = document.createElement('script');
im.type = 'importmap';
im.textContent = JSON.stringify({
    "imports": {
        "vue": "/js/vue.mjs",
        "@servicestack/client": "/js/servicestack-client.mjs",
        "@servicestack/vue": "/js/servicestack-vue.mjs"
    }
});
document.body.appendChild(im)

const [Vue, ServiceStack, ServiceStackVue, dtos] = await Promise.all([
    import('vue'),
    import('@servicestack/client'),
    import('@servicestack/vue'),
    import('/types/mjs')
])

const BookingsApp = {
  template:`<div class="mt-4 p-4 m-4"><h3 class="py-4 text-2xl text-center">Vue.js AutoQueryGrid</h3>
   <AutoQueryGrid type="Booking" selectedColumns="id,name,roomType,roomNumber,bookingStartDate,cost,couponId,discount"/>
  </div>`
}

const app = Vue.createApp(BookingsApp)
const client = new ServiceStack.JsonServiceClient()
app.provide('client', client)
app.use(ServiceStackVue.default)
ServiceStackVue.useMetadata().loadMetadata()
client.api(new dtos.Authenticate()).then(api => 
    ServiceStackVue.useAuth().signIn(api.response))

document.querySelector('main').insertAdjacentHTML('beforeend',`<div id="app"></div>`)
app.mount('#app')

After the modules and Vue App have loaded you should see the shiny new Vue.js AutoQueryGrid running inside a Blazor Server App!

An Effortless CRUD UI

Specifically this is a full Vue.js CRUD App running without any external dependencies, utilizing built-in Typed ES6 APIs without any tooling or build steps, generating a capability-based UI for the currently authenticated user, enabling full CRUD functionality through the App's auth-protected AutoQuery APIs, validated by the APIs Declarative Validation Rules utilizing optimal Form Inputs for each C# property type with contextual validation binding, that displays its results in a rich formatted data grid customized with APIs Declarative UI Attributes.

All this functionality is encapsulated in the new AutoQueryGrid component, which at a minimum just requires the AutoQuery Data Model:

<AutoQueryGrid type="Booking" />

Feature Rich without trade-offs

Whilst the Blazor and Vue AutoQueryGrid components might be functionally equivalent, they differ greatly in implementation. With Blazor having well-known trade-offs requiring either complex build tooling with slow iteration cycles and large multi megabyte downloads to get C# running in the browser with WASM or requiring a stateful rendering circuit with frequent disconnections and poor UX over laggy Internet connections with Blazor Server.

By contrast Vue.js being built with native web technologies doesn't have any of these trade-offs, where it can be used without any build tooling to yield instant dev iteration times. Like Blazor WASM performs its rendering on the client enabling a responsive UX without a stateful server circuit, but unlike Blazor WASM its cacheable assets loads instantly where all JavaScript libraries weighs in at a light 140kb:

gzip none
vue.mjs 58kb 129kb
servicestack-vue.mjs 63kb 174kb
servicestack-client.mjs 19kb 53kb

AutoQueryGrid Component

Despite it's small size, AutoQueryGrid is a highly customizable and versatile component allowing for multiple levels of customizations - documented with Live Examples in the new AutoQueryGrid Docs:

Read Only

Where apis can be used to restrict which APIs are used, where Read Only can be enabled by specifying to only use the Query API:

<AutoQueryGrid type="Booking" apis="QueryBookings"  />

Table Styles

The same DataGrid Table Styles can also be used to style AutoQueryGrid using tableStyle, e.g:

<AutoQueryGrid type="Booking" tableStyle="verticalLines,uppercaseHeadings" />

Custom Styles

The AutoQueryGrid's appearance is further customizable with the property classes & functions below:

defineProps<{
  toolbarButtonClass: string
  tableStyle: "simple" | "fullWidth" | "stripedRows" | "whiteBackground" | "uppercaseHeadings" | "verticalLines"
  gridClass: string
  grid2Class: string
  grid3Class: string
  grid4Class: string
  tableClass: string
  theadClass: string
  tbodyClass: string
  theadRowClass: string
  theadCellClass: string

  rowClass:(model:any,i:number) => string
  rowStyle:(model:any,i:number) => StyleValue
}>()

Custom AutoQueryGrid

AutoQueryGrid features can be hidden with hide or its functionality disabled with deny, e.g:

<AutoQueryGrid type="Booking" hide="pagingNav,copyApiUrl,downloadCsv" deny="filtering" />

Global Configuration

These features can also be disabled at a global level, applying to all <AutoQueryGrid> components with useConfig(), e.g:

const { setAutoQueryGridDefaults } = useConfig()

setAutoQueryGridDefaults({
  hide: ['pagingNav','copyApiUrl','downloadCsv']
})

Selected Columns

By default AutoQueryGrid displays all public properties returned in its AutoQuery API which can be further restricted with selectedColumns:

<AutoQueryGrid type="Booking" selectedColumns="id,name,roomType,roomNumber,cost" />

Simple Responsive Columns

Using visibleFrom is a simple way to enable a responsive DataGrid by specifying at which Tailwind breakpoints columns should be visible from and headerTitles to use friendlier aliases for different columns, e.g:

<AutoQueryGrid type="Booking" 
  selectedColumns="id,name,roomType,roomNumber,cost,bookingStartDate,bookingEndDate" 
  :headerTitles="{ roomNumber:'Room', bookingStartDate:'Start', bookingEndDate:'End' }"
  :visibleFrom="{ bookingStartDate:'lg', bookingEndDate:'xl' }" />

Custom Responsive Columns

Which columns are displayed and how they're formatted are further customizable with <template #column> slots:

<AutoQueryGrid type="Booking" :visibleFrom="{ name:'xl', bookingStartDate:'sm', bookingEndDate:'xl', createdBy:'2xl' }">
    <template #id="{ id }">
        <span class="text-gray-900">{‎{ id }‎}</span>
    </template>
    <template #name="{ name }">
        {‎{ name }‎}
    </template>
    <template #roomNumber-header>
        <span class="hidden lg:inline">Room </span>No
    </template>
    <template #cost="{ cost }">{‎{ currency(cost) }‎}</template>
    <template #bookingStartDate-header>
        Start<span class="hidden lg:inline"> Date</span>
    </template>
    <template #bookingEndDate-header>
        End<span class="hidden lg:inline"> Date</span>
    </template>
    <template #createdBy-header>
        Employee
    </template>
    <template #createdBy="{ createdBy }">{‎{ createdBy }‎}</template>
</AutoQueryGrid>

Custom Functionality

The column template slots can be leveraged to implement custom functionality, e.g. instead of navigating to separate pages to manage related data we can use a custom column to manage Booking Coupons from within the same grid, e.g:

<AutoQueryGrid type="Booking" selectedColumns="id,name,cost,bookingStartDate,bookingEndDate,discount">
    <template #discount="{ discount }">
        <TextLink v-if="discount" class="flex items-end" @click.stop="showCoupon(discount.id)" :title="discount.id">
            <Icon class="w-5 h-5 mr-1" type="Coupon" />
            <PreviewFormat :value="discount.description" />
        </TextLink>
    </template>
</AutoQueryGrid>
<AutoEditForm v-if="coupon" type="UpdateCoupon" v-model="coupon" @done="close" @save="close" />

<script setup lang="ts">
import { ref } from "vue"
import { useClient } from "@servicestack/vue"
import { QueryCoupons } from "dtos"

const client = useClient()
const coupon = ref()

async function showCoupon(id:string) {
    const api = await client.api(new QueryCoupons({ id }))
    if (api.succeeded) {
        coupon.value = api.response!.results[0]
    }
}

const close = () => coupon.value = null
</script>

Template Slots

AutoQueryGrid supports a number of Vue slots to customize its built-in UIs, including formheader and formfooter slots to insert custom content before and after the Auto Create & Edit components forms:

<template #formheader="{ form, type, apis, model, id }">
<template #formfooter="{ form, type, apis, model, id }">

This feature is used to implement Locode's Audit History UI for displaying the Audit History of each record in the bottom of the Edit Form for Authorized Users, implemented with:

<AutoQueryGrid :key="store.opDataModel" ref="grid" :type="store.opDataModel">
    <template #formfooter="{ form, type, apis, model, id }">
        <AuditEvents v-if="form === 'edit' && canAccessCrudEvents" class="mt-4" :key="id" :type="type" :id="id" />
    </template>
</AutoQueryGrid>

Alternatively you can replace the entire Create and Edit Forms used with the createform and editforms slots:

<template #createform="{ form, apis, type }">
<template #editform="{ form, apis, type }">

Additional toolbar buttons can be added with the toolbarbuttons slot, e.g:

<template #toolbarbuttons="{ toolbarButtonClass }">
  <div class="pl-2 mt-1">
      <button type="button" @click="customAction" :class="toolbarButtonClass">
        <span class="whitespace-nowrap">My Action</span>
      </button>
  </div>
</template>

Alternatively you can replace the entire toolbar with your own with:

<template #toolbar>

All other template slots are passed down to the embedded DataGrid component where they can customize column headers and cells.

AutoQueryGrid Data References Labels

Data References Labels is a new feature added to both Blazor and Vue AutoQueryGrid components, where Foreign Key Ids are replaced with human-friendly references from a string column in the Foreign Key referenced table which AutoQuery is able to infer from the POCO References relationships of your Data Models.

Where AutoQueryGrid will be able to make use of DataModel's including [Reference] properties so that its related Data is returned in its AutoQuery APIs to render reference labels instead of just raw FK Ids as seen in Job Application's JobId and ContactId columns:

Will now render the Contact and Job Names and Icons instead of their plain Foreign Key Ids:

<AutoQueryGrid type="JobApplication" :prefs="{take:5}" />

With the original ids are discoverable by hovering over the Job & Contact labels.

This was datagrid is rendered based on the JobApplication DataModel DTO:

[Icon(Svg = Icons.Application)]
public class JobApplication : AuditBase
{
    [AutoIncrement]
    public int Id { get; set; }

    [References(typeof(Job))]
    public int JobId { get; set; }

    [References(typeof(Contact))]
    public int ContactId { get; set; }

    [Reference]
    [Format(FormatMethods.Hidden)]
    public Job Position { get; set; }

    [Reference]
    [Format(FormatMethods.Hidden)]
    public Contact Applicant { get; set; }

    [Reference]
    public List<JobApplicationComment> Comments { get; set; }

    public DateTime AppliedDate { get; set; }

    public JobApplicationStatus ApplicationStatus { get; set; }
    //...
}

By default AutoQuery will infer using the first string column of the related table for its label, this information can also be explicitly defined with the [Ref] attribute, e.g:

public class JobApplication : AuditBase
{
    [AutoIncrement]
    public int Id { get; set; }

    [References(typeof(Job))]
    [Ref(Model=nameof(Job), RefId=nameof(Job.Id), RefLabel=nameof(Job.Title))]
    public int JobId { get; set; }

    [References(typeof(Contact))]
    [Ref(Model=nameof(Contact), RefId=nameof(Contact.Id), RefLabel=nameof(Contact.DisplayName))]
    public int ContactId { get; set; }
    //...
}

Alternatively you can use [Ref(None=true)] to disable any implicit inferences and render the FK property Ids as-is.

When displaying referential data you can tell AutoQueryGrid to hide rendering the complex data references as well columns using [Format(FormatMethods.Hidden)].

New Locode, API Explorer & Admin UIs now in Vue 3!

With access to all components needed, we've taken this opportunity to modernize all of ServiceStack's built-in UI's that were previously written with petite-vue and have rewritten them with Vue 3 and the Vue Components Library, which has become our preferred UI technology stack that all built-in UIs will be created with going forward.

Greater Customizability & Reuse

Ultimately the new Vue 3 UIs should now be a lot more appealing as you're better able to reuse, enhance and customize different components using the full Vue 3 framework and 3rd Party ecosystem for creating more compelling and interactive UIs:

Existing UIs

To ease upgrading, we've included the previous version which you can run side-by-side with the new version until all issues are resolved:

Locode API Explorer
v2 /locode /ui
v1 /locode-v1 /ui-v1

We intend to remove the older versions next release so please let us know if there's any outstanding issues with the new version.

AutoQuery and Vue Components Productivity Demo

The best-of-class combination of our new Vue 3 project templates, Tailwind CSS, Auto Form and AutoQueryGrid components make for a powerfully productive technology stack where tasks that would've traditionally have taken days to build can be done in just minutes.

As a demonstration, we've created a new video to accomplish a common task of adding a new Job Application feature to a new vue-mjs project template from scratch, utilizing:

Locode v2

Rewriting Locode v2 in Vue 3 significantly reduces development and maintenance efforts as all efforts are now invested in improving the single Vue 3 Component Library instead of being split between maintaining a separate embedded, non-reusable Petite Vue code-base.

Other benefits of using Vue 3 includes:

  • Using the more popular, capable and battle-tested Vue 3 flagship library that all our Vue Templates use
  • Access to richer components in the Vue Component Library
  • Access to an extendable and customizable AutoQueryGrid component
  • Custom Locode Components are now just normal Vue 3 components
  • Ability to reuse any custom Vue 3 components in bespoke Vue 3 Apps

Best of all if you ever decide to move beyond Locode to a bespoke solution you can take the custom AutoQueryGrid's with you or if preferred easily port it to a Blazor App using Blazor's AutoQueryGrid component which offers an equivalent featureset which should expand the potential use-cases that can benefit from starting with Locode.

To make upgrading easier we've visually kept (Vue 3) Locode v2 looking and behaving very similar to (petite-vue) Locode v1, the primary noticeable difference being that Forms now open in a Modal SlideOver dialog which is less visually jarring then moving the entire DataGrid up/down when opening/closing forms.

Otherwise Locode v2 implements most of Locode v1's features so any of your declarative UI customizations continue to be applied in v2:

Custom AutoQueryGrid

One area of customization that wasn't possible before is being able to easily replace entire Locode Pages with your own, which thanks to the new AutoQueryGrid component takes very little effort to do. In fact since everything's now using the same libraries we can copy the custom Bookings AutoQueryGrid Vue 3 component in the new vue-mjs Project Template and use it to manage our Bookings in Locode by registering a Vue 3 component with the name:

{DataModel}Page

That we can auto register with Locode by adding it in our /wwwroot folder at /modules/locode/components/*.mjs.

Which we've added in our Blazor Gallery App, in /modules/locode/components/BookingPage.mjs containing our custom Bookings AutoQueryGrid component:

import { inject, ref } from "vue"
import { QueryCoupons } from "/types/mjs"

export const BookingPage = {
    template:`
    <div>
        <h1 class="py-8 text-center text-3xl text-indigo-700 font-semibold">Custom Bookings AutoQueryGrid</h1>
        <AutoQueryGrid type="Booking" selected-columns="id,name,cost,bookingStartDate,bookingEndDate,discount,notes">
        <template #discount="{ discount }">
            <TextLink v-if="discount" class="flex items-end" @click.stop="showCoupon(discount.id)" :title="discount.id">
                <Icon class="w-5 h-5 mr-1" type="Coupon" />
                <PreviewFormat :value="discount.description" />
            </TextLink>
        </template>
        </AutoQueryGrid>
        <AutoEditForm v-if="coupon" type="UpdateCoupon" v-model="coupon" @done="close" @save="close" />
    </div>
    `,
    props:['type'],
    setup() {
        const client = inject('client')
        const coupon = ref()
        async function showCoupon(id) {
            const api = await client.api(new QueryCoupons({ id }))
            if (api.succeeded) {
                coupon.value = api.response.results[0]
            }
        }
        const close = () => coupon.value = null
        return { coupon, showCoupon, close }
    }
}

Where our custom version will open the related Coupon entry for the booking allowing both Bookings and their Coupons to be managed from the same page.

Now when Booking is selected in Locode it will load our custom version:

Custom Forms

Most of the effort required will be converting any custom Petite Vue HTML Components into Vue 3 components which we'll walk through showing how we converted Locode v1 Components.

Custom Forms continue to use the existing naming convention, but now need to be created as Vue 3 components added to your Host Project /wwwroot folder at /modules/locode/components/*.mjs using the naming conventions below:

Component Name Description
New{Table} Custom Create Form UI
Edit{Table} Custom Update Form UI

The chinook.locode.dev demo does this to create a custom Form UI for creating and editing Albums by registering NewAlbums in NewAlbums.mjs and EditAlbums component in EditAlbums.mjs which are used to render Chinook's custom Create Album form to update its Albums table.

JavaScript Libraries

Your custom components can utilize built in libraries embedded in ServiceStack.dll where they will have access to the latest Vue 3 reactive fx, @servicestack/client client library and Vue 3 Tailwind Component library which they can import by package name, e.g:

import { ref } from "vue"
import { useClient } from "@servicestack/vue"
import { humanify } from "@servicestack/client"

Static Analysis

As all package dependencies are written in TypeScript you can install them as dev dependencies to get static analysis from its TypeScript definitions at dev time:

npm install -D vue
npm install -D @servicestack/client
npm install -D @servicestack/vue

Your components can access your Apps Typed DTOs directly from the ES6 Module DTO endpoint at /types/mjs, e.g:

import { CreateAlbums } from "/types/mjs"

App functionality

Your components access to most App functionality via the injected dependencies for functionality defined in Locode's app.mjs:

const app = inject('app')                  // App for customizing Vue App, register components, providers, plugins, etc
const client = inject('client')            // JsonServiceClient for API Calls
const server = inject('server')            // AppMetadata (metadata for your Server App and APIs)
const store = inject('store')              // Locode's Reactive object model
const routes = inject('routes')            // usePageRoutes() Reactive store to manage its SPA routing
const breakpoints = inject('breakpoints')  // useBreakpoints() Reactive store to Tailwind responsive breakpoints

Most of which creates instance of common library features in core.mjs that are documented at api.locode.dev/modules/locode.html.

You're also not limited with what's in Locode, with full access to JavaScript Modules you can import external 3rd Party packages the same way you import built-in packages.

Code walkthrough

Your components will have full control to implement their desired functionality how they want, which can get a lot of integrated functionality for free by leveraging the Vue Component library, e.g. this custom component uses:

  • ModalDialog - to load our custom Create Albums Form component in a Modal Dialog
  • ErrorSummary - to display any non-contextual summary API errors
  • TextInput - to create a validation bound form for the CreateAlbums Title property
  • LookupInput - to create a Lookup input to select an Artist for the CreateAlbums ArtistId property

Whilst <SubmitAlbumButton> is an example of using a shared component in SubmitAlbumButton.mjs.

import { ref } from "vue"
import { useClient, useMetadata } from "@servicestack/vue"
import { CreateAlbums } from "/types/mjs"

export const NewAlbums = {
    template:/*html*/`
    <ModalDialog @done="done" sizeClass="">
      <div class="album-form relative flex flex-col">
        <ErrorSummary except="title,artistId"/>
        <form @submit.prevent="submit" class="m-4 shadow-md rounded-full w-96 h-96 flex justify-center items-center">
          <div class="flex flex-col justify-center items-center text-center">
            <h1 class="text-3xl font-medium text-rose-500 mb-4">New Album</h1>
            <fieldset>
              <TextInput id="title" v-model="request.title" label="" placeholder="Album Title" class="mb-3" />

              <LookupInput id="artistId" v-model="request" label="" placeholder="Select Artist"
                           :input="lookupProp.input" :metadataType="dataModelType" class="mb-3" />

              <SubmitAlbumButton />
            </fieldset>
          </div>
        </form>
      </div>
    </ModalDialog>
    `,
    props: ['type'],
    emits: ['done','save'],
    setup(props, { emit }) {
        const client = useClient()
        const { typeOf } = useMetadata()

        const dataModelType = typeOf("Albums")
        const lookupProp = dataModelType.properties.find(x => x.name === 'ArtistId')
        const request = ref(new CreateAlbums())

        /** @param {Event} e */
        async function submit(e) {
            const form = e.target
            const api = await client.apiForm(new CreateAlbums(), new FormData(form))
            if (api.succeeded) {
                emit('save', api.response)
            }
        }

        function done() {
            emit('done')
        }

        return { request, lookupProp, dataModelType, submit, done }
    }
}

The only integration needed to communicate back with Locode's AutoQueryGrid component is to emit done when the form is dismissed without changes or emit save if changes are made to refresh the AutoQueryGrid resultset to see the latest changes.

Invoking APIs with useClient() APIs will propagate any error information from any declarative validation attributes into validation-aware components which alleviates us from needing to perform any manual validation ourselves.

When registered this custom component replaces Locode's Auto Form UI with a custom Create Album Form:

That when submitting an empty form will trigger the contextual validation errors to appear:

As enforced by the Declarative Validation rules on the CreateAlbums AutoQuery CRUD DTO its calling:

[Route("/albums", "POST"), Tag(Tags.Media)]
public class CreateAlbums
    : IReturn<IdResponse>, IPost, ICreateDb<Albums>
{
    [ValidateNotEmpty]
    public string Title { get; set; }

    [ValidateGreaterThan(0)]
    public long ArtistId { get; set; }
}

Custom Edit Form

The custom EditAlbums form has a very similar implementation the NewAlbums implementation above other than populating the Input components with the existing Album's values, accessible via the model property.

import { useClient, useMetadata } from "@servicestack/vue"
import { ref } from "vue"
import { UpdateAlbums } from "/types/mjs"

export const EditAlbums = {
    template:/*html*/`
      <ModalDialog @done="done" sizeClass="">
        <div class="album-form relative flex flex-col">
          <ErrorSummary except="title,artistId" />
          <form @submit.prevent="submit" class="m-4 shadow-md rounded-full w-96 h-96 max-w-96 flex justify-center items-center">
            <div class="flex flex-col justify-center items-center text-center">
              <h1 class="text-3xl font-medium text-rose-500 mb-4">Edit Album {‎{ request.albumId }‎}</h1>
              <fieldset>
                <input type="hidden" name="albumId" :value="request.albumId">
                <TextInput id="title" v-model="request.title" label="" placeholder="Album Title" class="mb-3" />

                <LookupInput id="artistId" v-model="request" label="" placeholder="Select Artist"
                             :input="lookupProp.input" :metadataType="dataModelType" class="mb-3" />

                <SubmitAlbumButton />
              </fieldset>
            </div>
          </form>
        </div>
      </ModalDialog>
    `,
    props: ['model','type','deleteType'],
    emits: ['done','save'],
    setup(props, { emit }) {
        const client = useClient()
        const { typeOf } = useMetadata()

        const dataModelType = typeOf("Albums")
        const lookupProp = dataModelType.properties.find(x => x.name === 'ArtistId')
        const request = ref(new UpdateAlbums(props.model))

        /** @param {Event} e */
        async function submit(e) {
            const form = e.target
            const api = await client.apiForm(new UpdateAlbums(), new FormData(form))
            if (api.succeeded) {
                emit('save', api.response)
            }
        }

        function done() {
            emit('done')
        }

        return { request, lookupProp, dataModelType, submit, done }
    }
} 

If applicable deleteType will be populated with an API to Delete Albums that the current user has authorization to access, should you wish to implement delete functionality.

Then to perform the updates we just need to call an Update Albums API, which for Chinook is called PatchAlbums:

[Route("/albums/{AlbumId}", "PATCH"), Tag(Tags.Media)]
public class PatchAlbums
    : IReturn<IdResponse>, IPatch, IPatchDb<Albums>
{
    public long AlbumId { get; set; }
    public string Title { get; set; }
    public long ArtistId { get; set; }
}

Which is all that's need to implement our custom Edit Albums Form:

To minimize code duplication both custom forms makes use of a shared SubmitAlbumButton.mjs component, defined as:

export const SubmitAlbumButton = {
    template:`
    <button type="submit" class="inline-flex items-center p-3 border border-transparent rounded-full shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">
        <svg class="h-8 w-8" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" preserveAspectRatio="xMidYMid meet" viewBox="0 0 48 48">
            <g fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="4">
                <path stroke-linecap="round" d="M24 44C12.954 44 4 35.046 4 24S12.954 4 24 4s20 8.954 20 20"/>
                <path d="M20 24v-6.928l6 3.464L32 24l-6 3.464l-6 3.464V24Z"/><path stroke-linecap="round" d="M37.05 32v10M42 36.95H32"/>
            </g>
        </svg>
    </button>`
}

Overriding built-in components

The Chinook Demo also shows an example of overriding its existing /modules/locode/components/Welcome.mjs component in order to render its custom Home page.

Which in addition to using built-in Locode functionality, also makes use of your Apps Typed DTOs directly from the ES6 Module DTO endpoint at /types/mjs, e.g:

import { QueryInvoices } from "/types/mjs"

This results in providing an end-to-end typed dev UX for creating custom components that call our App's APIs as done in:

import { inject, ref, onMounted, computed } from "vue"
import { QueryInvoices } from "/types/mjs"

export const Welcome = {
    template:/*html*/`
    <div class="pl-4">
        <h1 class="text-3xl">
            Welcome to Chinook Locode
        </h1>
        <div v-if="lastOrders.length" class="mt-8">
            <h3 class="text-xl mb-4">Here are your last {‎{lastOrders.length}‎} orders:</h3>
            <DataGrid class="max-w-screen-md" type="Invoices" :items="lastOrders" tableStyle="uppercaseHeadings" />
        </div>
    </div>`,
    setup() {
        const client = inject('client')
        const api = ref()
        const lastOrders = computed(() => api.value?.response?.results || [])
        
        onMounted(async () => {
            api.value = await client.api(new QueryInvoices({ 
                orderBy:'-InvoiceId',
                take:5,
                fields:'InvoiceId,CustomerId,InvoiceDate,Total,BillingCountry,BillingCity'
            }), { jsconfig: 'edv' })
        })
        
        return { lastOrders }
    }
}

Which uses the DataGrid component to render its custom Home page:

That makes use of the Declarative UI Attributes in its Invoices data model to render a formatted currency Total and a direct link to the Customer that the invoice was for.

API Explorer v2

The primary effort in upgrading to use API Explorer v2 will be converting any existing petite-vue API Docs into Vue 3 components which should be added to your Host projects local /modules/ui/docs folder which the Blazor WASM project template includes to showcase some customization examples:

Where you can enhance any of your APIs or DTOs with rich API docs by adding Vue Components to /modules/ui/docs/*.mjs which gets included together with API Explorers own components in its single file download.

For auto registration of components the *.mjs should match the API doc component which needs to be named:

`

Here's a simple CreateBookingDocs.mjs component example used to generate API Docs for the CreateBooking API:

Which just references server AppMetadata to access server API info from the /metadata/app.json endpoint:

import { inject } from "vue"

export const CreateBookingDocs = {
    template:`
    <div class="text-center my-3">
        <div class="flex justify-center">
            <svg class="w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
                <path fill="#CFD8DC" d="M5 38V14h38v24c0 2.2-1.8 4-4 4H9c-2.2 0-4-1.8-4-4z"/><path fill="#F44336" d="M43 10v6H5v-6c0-2.2 1.8-4 4-4h30c2.2 0 4 1.8 4 4z"/>
                <g fill="#B71C1C"><circle cx="33" cy="10" r="3"/><circle cx="15" cy="10" r="3"/></g>
                <g fill="#B0BEC5"><path d="M33 3c-1.1 0-2 .9-2 2v5c0 1.1.9 2 2 2s2-.9 2-2V5c0-1.1-.9-2-2-2z"/><path d="M15 3c-1.1 0-2 .9-2 2v5c0 1.1.9 2 2 2s2-.9 2-2V5c0-1.1-.9-2-2-2z"/></g>
                <g fill="#90A4AE"><path d="M13 20h4v4h-4z"/><path d="M19 20h4v4h-4z"/><path d="M25 20h4v4h-4z"/><path d="M31 20h4v4h-4z"/><path d="M13 26h4v4h-4z"/><path d="M19 26h4v4h-4z"/><path d="M25 26h4v4h-4z"/><path d="M31 26h4v4h-4z"/><path d="M13 32h4v4h-4z"/><path d="M19 32h4v4h-4z"/><path d="M25 32h4v4h-4z"/><path d="M31 32h4v4h-4z"/></g>
            </svg>
            <h2 class="text-3xl ml-3 mb-3">Create Bookings API</h2>
        </div>
        <div class="text-gray-500 text-lg">
            <p>
                Create a new room Booking for our {‎{serviceName}‎} hotels.
            </p>
            <p>
                Here are some
                <a class="svg-external text-blue-800" target="_blank"
                    href="https://edition.cnn.com/travel/article/scoring-best-hotel-rooms/index.html">
                    good tips on making room reservations
                </a>
            </p>
        </div>
    </div>
    `,
    setup() {
        const server = inject('server')
        return { serviceName: server.app.serviceName }
    }
}

Dynamic Components

QueryTodos is a more advanced example that generates a dynamic UI shared by all TODO APIs that generate its reactive Mini Navigation UI users can use to cycle through all TODO API docs with a v-href="{ op }" custom directive:

Where instead of registering a single component, it exports a install(app) function to register API Doc components for all TODO CRUD APIs, all registered with the same TodosDocs component:

import { inject, computed } from "vue"
import { humanize } from "@servicestack/client"

export function install(app) {
    const apis = {
        QueryTodos:  'Query Todos, returns all Todos by default',
        CreateTodo:  'Create a Todo',
        UpdateTodo:  'Update a Todo',
        DeleteTodo:  'Delete Todo by Id',
        DeleteTodos: 'Delete multiple Todos by Ids',
    }
    const apiNames = Object.keys(apis)
    const TodosDocs = {
        template:`
        <div class="mx-auto max-w-screen-md text-center py-8">
            <h2 class="text-center text-3xl">{‎{humanize(op.request.name)}‎}</h2>
            <p class="text-gray-500 text-lg my-3">{‎{apis[op.request.name]}‎}</p>
            <div class="flex justify-center text-left">
                <table>
                    <caption class="mt-3 text-lg font-normal">Other Todo APIs</caption>
                    <tr v-for="(info,name) in otherApis">
                        <th class="text-right font-medium pr-3">
                            <a v-href="{ op:name }" class="text-blue-800">{‎{humanize(name)}‎}</a>
                        </th>
                        <td class="text-gray-500">{‎{info}‎}</td>
                    </tr>
                </table>
            </div>
        </div>`,
        setup() {
            const store = inject('store')
            const op = computed(() => store.op)
            const otherApis = computed(() => apiNames.filter(x => x !== store.op.request.name)
                 .reduce((acc,x) => { acc[x] = apis[x]; return acc }, {}))
            return { 
                op,
                apis,
                otherApis,
                humanize,
            }
        }
    }
    const components = apiNames.reduce((acc, x) => { acc[x + 'Docs'] = TodosDocs; return acc }, {})
    app.components(components)
}

Built-in API Docs

ServiceStack's own built-in APIs uses custom API Doc components itself to document its APIs, e.g. /ui/docs/RegisterDocs.mjs

export const RegisterDocs = {
    template:`
      <div class="max-w-screen-md mx-auto text-center">
          <h2 class="text-2xl font-medium mb-3">Register API</h2>
          <p class="text-gray-500">
            Public API users can use to create a new User Account, can be added to your AppHost with:
          </p>
          <pre class="my-3"><code v-highlightjs="'Plugins.Add(new RegistrationFeature());'"></code></pre>
      </div>`
}

Generates docs for the built-in Register API that includes C# Syntax highlighting using the pre-configured highlightjs directive:

Whilst /ui/docs/AuthenticateDocs.mjs demonstrates a more advanced example in generating a responsive dynamic tab layout containing multiple relevant ServiceStack Auth YouTube videos:

Override built-in Components

The built-in UIs also lets you override existing components by adding custom versions in /js/components e.g.

const Brand = {
    template:/*html*/`
    <div class="flex items-center flex-shrink-0 max-w-sidebar">
        <a title="My App" v-href="{ $page:'' }"
           class="text-2xl whitespace-nowrap overflow-x-hidden flex items-center">
           <svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 ml-1 mr-2" viewBox="0 0 24 24">
               <path d="M23.834 8.101a13.912 13.912 0 0 1-13.643 11.72a10.105 10.105 0 0 1-1.994-.12a6.111 6.111 0 0 1-5.082-5.761a5.934 5.934 0 0 1 11.867-.084c.025.983-.401 1.846-1.277 1.871c-.936 0-1.374-.668-1.374-1.567v-2.5a1.531 1.531 0 0 0-1.52-1.533H8.715a3.648 3.648 0 1 0 2.695 6.08l.073-.11l.074.121a2.58 2.58 0 0 0 2.2 1.048a2.909 2.909 0 0 0 2.695-3.04a7.912 7.912 0 0 0-.217-1.933a7.404 7.404 0 0 0-14.64 1.603a7.497 7.497 0 0 0 7.308 7.405s.549.05 1.167.035a15.803 15.803 0 0 0 8.475-2.528c.036-.025.072.025.048.061a12.44 12.44 0 0 1-9.69 3.963a8.744 8.744 0 0 1-8.9-8.972a9.049 9.049 0 0 1 3.635-7.247a8.863 8.863 0 0 1 5.229-1.726h2.813a7.915 7.915 0 0 0 5.839-2.578a.11.11 0 0 1 .059-.034a.112.112 0 0 1 .12.053a.113.113 0 0 1 .015.067a7.934 7.934 0 0 1-1.227 3.549a.107.107 0 0 0-.014.06a.11.11 0 0 0 .073.095a.109.109 0 0 0 .062.004a8.505 8.505 0 0 0 5.913-4.876a.155.155 0 0 1 .055-.053a.15.15 0 0 1 .147 0a.153.153 0 0 1 .054.053A10.779 10.779 0 0 1 23.834 8.1zM8.895 11.628a2.188 2.188 0 1 0 2.188 2.188v-2.042a.158.158 0 0 0-.15-.15z" fill="#5C2D91"/>
            </svg>
           My App
        </a>
    </div>
    `
}
export default Brand

Which override's the built-in Brand component to replace the Logo on the top-right of API Explorer, Locode and the Admin UI with the custom version below:

Admin UI v2

As no customizations or extensibility features were available for the built-in Admin UIs, no effort should be required to upgrade.

If you do find any issues with the new version please let us know in the Customer Forums, where they'll be promptly resolved.

API Index

Previously calling the JSON /api pre-defined route without the name of the API to call resulted in an error, we're now using it as an opportunity to improve API Discovery by returning an App's public APIs grouped by user-defined tags with direct links to the API endpoint, viewing it in API Explorer and in Locode for AutoQuery APIs, e.g:

https://example.org/api

New Vue Components

In addition to the AutoQueryGrid

AutoForm

The AutoForm component is a generic form component (used extensively in API Explorer) that can be used to create and wire a traditional Form for any Request DTO definition where successful responses can be handled the @success event, e.g:

<AutoForm type="QueryBookings" @success="onSuccess" />
<div v-if="results">
    <h3 class="py-4 text-2xl">Results</h3>
    <HtmlFormat :value="results" />
</div>

<script setup>
const results = ref([])
const onSuccess = response => results.value = response.results
</script>

Results

See the AutoForm Component docs for different enhancement & customizability options.

Combobox

The Combobox component provides an Autocomplete Input optimized for searching a List of string values, Key Value Pairs or Object Dictionary, e.g:

<div class="grid grid-cols-12 gap-6">
  <Combobox id="Strings" class="col-span-4" v-model="strings" :values="['Alpha','Bravo','Charlie']" />
  <Combobox id="Object"  class="col-span-4" v-model="objects" :options="{ A:'Alpha', B:'Bravo', C:'Charlie' }" />
  <Combobox id="Pairs"   class="col-span-4" v-model="pairs"   label="Multiple from Pairs" multiple
    :entries="[{key:'A',value:'Alpha'}, {key:'B',value:'Bravo'}, {key:'C',value:'Charlie'}]" />
</div>

Which supports populating both a single string value or multiple strings in an Array with multiple property.

Auto Forms

Combobox components can also be used in Auto Form Components on string or string collection properties with the [Input(Type="combobox")] declarative UI Attribute on C# Request DTOs, e.g:

public class ComboBoxExamples : IReturn<ComboBoxExamples>, IPost
{
    [Input(Type="combobox", Options = "{ allowableValues:['Alpha','Bravo','Charlie'] }")]
    public string? SingleClientValues { get; set; }

    [Input(Type="combobox", Options = "{ allowableValues:['Alpha','Bravo','Charlie'] }", Multiple = true)]
    public List<string>? MultipleClientValues { get; set; }

    [Input(Type="combobox", EvalAllowableValues = "['Alpha','Bravo','Charlie']")]
    public string? SingleServerValues { get; set; }

    [Input(Type="combobox", EvalAllowableValues = "AppData.AlphaValues", Multiple = true)]
    public List<string>? MultipleServerValues { get; set; }

    [Input(Type="combobox", EvalAllowableEntries = "{ A:'Alpha', B:'Bravo', C:'Charlie' }")]
    public string? SingleServerEntries { get; set; }

    [Input(Type="combobox", EvalAllowableEntries = "AppData.AlphaDictionary", Multiple = true)]
    public List<string>? MultipleServerEntries { get; set; }
}

Which can then be rendered with:

<AutoForm type="ComboBoxExamples" />

Combobox Options

Each property shows a different way of populating the Combobox's optional values, they can be populated from a JavaScript Object literal using Options or on the server with a #Script Expression where they can be populated from a static list or from a C# class as seen in the examples referencing AppData properties:

public class AppData
{
    public List<string> AlphaValues { get; set; }
    public Dictionary<string, string> AlphaDictionary { get; set; }
    public List<KeyValuePair<string, string>> AlphaKeyValuePairs { get; set; }
}

Which are populated on in the AppHost on Startup with:

ScriptContext.Args[nameof(AppData)] = new AppData
{
    AlphaValues = new() {
        "Alpha", "Bravo", "Charlie"
    },
    AlphaDictionary = new()
    {
        ["A"] = "Alpha",
        ["B"] = "Bravo",
        ["C"] = "Charlie",
    },
    AlphaKeyValuePairs = new()
    {
        new("A","Alpha"),
        new("B","Bravo"),
        new("C","Charlie"),
    },
};

Which can alternatively be populated from a dynamic source like an RDBMS table.

As C# Dictionaries have an undetermined sort order, you can use a List<KeyValuePair<string, string>> instead when you need to display an ordered list of Key/Value pairs.

TagInput with allowable values

The TagInput has also gained the ability to select from a list of options, e.g:

<div class="shadow sm:rounded-md bg-white dark:bg-black">
    <div class="relative px-4 py-5 sm:p-6">
        <div class="grid grid-cols-12 gap-6">
            <div class="col-span-12">
                <TagInput v-model="request.skills" label="Technology Skills"
                    :allowableValues="['c#','servicestack','vue','.net','typescript']" />
            </div>
        </div>
    </div>
</div>

The list of allowable values can also be populated on C# Request DTO from a JavaScript expression:

public class MyRequest
{
    [Input(Type = "tag", Options="{ allowableValues: ['c#','servicestack','vue'] }")]
    public List<string>? Skills { get; set; }
}

Or from a #Script Expression in EvalEvalAllowableValues where it can be populated from a static list, e.g:

public class MyRequest
{
    [Input(Type = "tag", EvalEvalAllowableValues="['c#','servicestack','vue']")]
    public List<string>? Skills { get; set; }
}

Or sourced from a C# Expression, e.g:

public class MyRequest
{
    [Input(Type = "tag", EvalEvalAllowableValues="AppData.Tags")]
    public List<string>? Skills { get; set; }
}

Where it can be populated from a dynamic data source like from an RDBMS populated in your AppHost on Startup, e.g:

ScriptContext.Args[nameof(AppData)] = new AppData {
    Tags = db.Select<Tag>().Select(x => x.Name).ToList()
};

Tabs

The Tabs component lets you switch between different Vue components from a object component dictionary where the Key is used for the Tab's label and URL param and the Value component for the tab body.

<script setup>
import A from "./A.vue"
import B from "./B.vue"
import C from "./C.vue"
const tabs = { A, B, C }
</script>

The Tab's Label can alternatively be overridden with a custom label function, e.g:

<Tabs :tabs="tabs" :label="tab => `${tab} Tab Label`" />

SignIn

The SignIn Component can be used to create an instant Sign Up form based on the registered Auth Providers that handles Signing In authenticated users into Vue Apps with the useAuth() APIs:

<SignIn v-if="!user" />
<div v-else>
    <h3 class="text-2xl my-4">Hello, {‎{ user.displayName }‎}</h3>
    <PrimaryButton @click="signOut()">Sign Out</PrimaryButton>
</div>

<script setup>
import { useAuth } from "@servicestack/vue"
const { user, signOut } = useAuth()
</script>

Hello, {‎{ user.displayName }‎}

Sign Out

Angular 15

The angular-spa project template has been upgraded to Angular 15:

It can be created from the servicestack.net/start page or on the command-line with the x tool:

x new angular-spa ProjectName

AutoQuery Collections not initialized by Default

As it's common for AutoQuery APIs to potentially have a large number of optional collection properties, having them initialized by default results in sending empty collections on the wire, which is unnecessary as they have the same behavior as if nothing was sent when the property was omitted entirely.

Since we added this feature C# introduced Target-Typed New Expressions which reduces the boilerplate to initialize collections, e.g:

var request = new QueryTable {
    Ids = new() { id1, id2, id3 },
};

As a result ServiceStack no longer initializes AutoQuery Collections by default, the previous behavior can be restored with:

ConfigurePlugin<NativeTypesFeature>(feature => 
  feature.ShouldInitializeCollection=NativeTypesFeature.AllCollectionProperties);