useClient() provides managed APIs around the JsonServiceClient
instance registered in Vue App's with:
app.provide('client', client)
Which maintains contextual information around your API calls like loading and error states, used by @servicestack/vue
components to
enable its auto validation binding. Other functionality in this provider include:
let {
api, // Send a typed API request and return results in an ApiResult<TResponse>
apiVoid, // Send a typed API request and return empty response in a void ApiResult
apiForm, // Send a FormData API request and return results in an ApiResult<TResponse>
apiFormVoid, // Send a FormData API request and return empty response in a void ApiResult
loading, // Maintain loading state whilst API Request is in transit
error, // Maintain API Error response in reactive Ref<ResponseStatus>
setError, // Set API error state with summary or field validation error
addFieldError, // Add field error to API error state
unRefs // Returns a dto with all Refs unwrapped
} = useClient()
Typically you would need to unwrap ref
values when calling APIs, i.e:
let client = new JsonServiceClient()
let api = await client.api(new Hello({ name:name.value }))
api​
This is unnecessary in useClient api*
methods which automatically unwraps ref values, allowing for the more pleasant API call:
let api = await client.api(new Hello({ name }))
unRefs​
But as DTOs are typed, passing reference values will report a type annotation warning in IDEs with type-checking enabled,
which can be avoided by explicitly unwrapping DTO ref values with unRefs
:
let api = await client.api(new Hello(unRefs({ name })))
setError​
setError
can be used to populate client-side validation errors which the
SignUp.mjs
component uses to report an invalid submissions when passwords don't match:
const { api, setError } = useClient()
async function onSubmit() {
if (password.value !== confirmPassword.value) {
setError({ fieldName:'confirmPassword', message:'Passwords do not match' })
return
}
//...
}
Form Validation​
All @servicestack/vue
Input Components support contextual validation binding that's typically populated from API
Error Response DTOs but can also be populated from client-side validation
as done above.
Explicit Error Handling​
This populated ResponseStatus
DTO can either be manually passed into each component's status property as done in /Todos:
<template id="TodoMvc-template">
<div class="mb-3">
<text-input :status="store.error" id="text" label="" placeholder="What needs to be done?"
v-model="store.newTodo" v-on:keyup.enter.stop="store.addTodo()"></text-input>
</div>
<!-- ... -->
</template>
Where if you try adding an empty Todo the CreateTodo
API will fail and populate its store.error
reactive property with the
APIs Error Response DTO which the <TextInput />
component checks for to display any field validation errors matching the
field in id
adjacent to the HTML Input:
let store = {
/** @type {Todo[]} */
todos: [],
newTodo:'',
error:null,
async refreshTodos(errorStatus) {
this.error = errorStatus
let api = await client.api(new QueryTodos())
if (api.succeeded)
this.todos = api.response.results
},
async addTodo() {
this.todos.push(new Todo({ text:this.newTodo }))
let api = await client.api(new CreateTodo({ text:this.newTodo }))
if (api.succeeded)
this.newTodo = ''
return this.refreshTodos(api.error)
},
//...
}
Implicit Error Handling​
More often you'll want to take advantage of the implicit validation support in useClient()
which makes its state available to child
components, alleviating the need to explicitly pass it in each component as seen in razor tailwind's
Contacts.mjs Edit
component for its
/Contacts page which doesn't do any manual error handling:
const Edit = {
template:/*html*/`<SlideOver @done="close" title="Edit Contact">
<form @submit.prevent="submit">
<input type="submit" class="hidden">
<fieldset>
<ErrorSummary except="title,name,color,filmGenres,age,agree" class="mb-4" />
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<SelectInput id="title" v-model="request.title" :options="enumOptions('Title')" />
</div>
<div class="col-span-6 sm:col-span-3">
<TextInput id="name" v-model="request.name" required placeholder="Contact Name" />
</div>
<div class="col-span-6 sm:col-span-3">
<SelectInput id="color" v-model="request.color" :options="colorOptions" />
</div>
<div class="col-span-6 sm:col-span-3">
<SelectInput id="favoriteGenre" v-model="request.favoriteGenre" :options="enumOptions('FilmGenre')" />
</div>
<div class="col-span-6 sm:col-span-3">
<TextInput type="number" id="age" v-model="request.age" />
</div>
</div>
</fieldset>
</form>
<template #footer>
<div class="flex justify-between space-x-3">
<div><ConfirmDelete @delete="onDelete">Delete</ConfirmDelete></div>
<div><PrimaryButton @click="submit">Update Contact</PrimaryButton></div>
</div>
</template>
</SlideOver>`,
props:['contact'],
emits:['done'],
setup(props, { emit }) {
const client = useClient()
const request = ref(new UpdateContact(props.contact))
const colorOptions = propertyOptions(getProperty('UpdateContact','Color'))
async function submit() {
const api = await client.api(request.value)
if (api.succeeded) close()
}
async function onDelete () {
const api = await client.apiVoid(new DeleteContact({ id:props.id }))
if (api.succeeded) close()
}
const close = () => emit('done')
return { request, enumOptions, colorOptions, submit, onDelete, close }
}
}
This effectively makes form validation binding a transparent detail where all @servicestack/vue
Input Components are able to automatically apply contextual validation errors next to the fields they apply to:
Example using apiForm​
An alternative method of invoking APIs is to submit a HTML Form Post which can be achieved with Ajax by sending a populated FormData
with client.apiForm()
as done in vue-mjs's
SignUp.mjs for its
/signup page:
import { ref } from "vue"
import { leftPart, rightPart, toPascalCase } from "@servicestack/client"
import { useClient } from "@servicestack/vue"
import { Register } from "../mjs/dtos.mjs"
export default {
template:/*html*/`
<form @submit.prevent="submit">
<div class="shadow overflow-hidden sm:rounded-md">
<ErrorSummary except="displayName,userName,password,confirmPassword,autoLogin" />
<div class="px-4 py-5 bg-white dark:bg-black space-y-6 sm:p-6">
<div class="flex flex-col gap-y-4">
<TextInput id="displayName" help="Your first and last name" v-model="request.displayName" />
<TextInput id="userName" label="Email" placeholder="Email" help="" v-model="request.userName" />
<TextInput id="password" type="password" help="6 characters max" v-model="request.password "/>
<TextInput id="confirmPassword" type="password" v-model="request.confirmPassword" />
<CheckboxInput id="autoLogin" v-model="request.autoLogin" />
</div>
</div>
<div class="pt-5 px-4 py-3 bg-gray-50 dark:bg-gray-900 text-right sm:px-6">
<div class="flex justify-end">
<FormLoading v-if="loading" class="flex-1" />
<PrimaryButton :disabled="loading" class="ml-3">Sign Up</PrimaryButton>
</div>
</div>
</div>
</form>`,
props: { returnUrl:String },
setup(props) {
const client = useClient()
const { setError, loading } = client
const request = ref(new Register({ autoLogin:true }))
/** @param email {string} */
function setUser(email) {
let first = leftPart(email, '@')
let last = rightPart(leftPart(email, '.'), '@')
const dto = request.value
dto.displayName = toPascalCase(first) + ' ' + toPascalCase(last)
dto.userName = email
dto.confirmPassword = dto.password = 'p@55wOrd'
}
/** @param {Event} e */
async function submit(e) {
if (request.value.password !== request.value.confirmPassword) {
setError({ fieldName: 'confirmPassword', message: 'Passwords do not match' })
return
}
// Example using client.apiForm()
const api = await client.apiForm(new Register(), new FormData(e.target))
if (api.succeeded) {
location.href = props.returnUrl || '/signin'
}
}
return { loading, request, setUser, submit }
}
}
Which method to use is largely a matter of preference except if your form needs to upload a file in which case using apiForm
is required.
AutoForm Components​
We can elevate our productivity even further with Auto Form Components that can automatically generate an instant API-enabled form with validation binding by just specifying the Request DTO to create the form for, e.g:
<AutoCreateForm type="CreateBooking" formStyle="card" />
The AutoForm components are powered by your App Metadata which allows creating highly customized UIs from declarative C# attributes whose customizations are reused across all ServiceStack Auto UIs.
Stale-While-Revalidate APIs​
A popular performance enhancing technique you can use to improve perceived performance between pages are to use State-While-Revalidate (SWR) APIs which can deliver just as good UX as complex SPAs with stateless full page reloads of pre-rendered HTML pages if we use SWR to fetch all the API data needed to render the page on first load:
This is easily achieved in reactive Vue.js UIs by invoking API requests with the new swr()
client API where if
the same API request had been run before it will execute the callback immediately with its "stale" cached results
in localStorage
first, before invoking the callback again after receiving the API response with the latest data:
import { useClient } from "@servicestack/vue"
const client = useClient()
const results = ref([])
const topAlbums = ref([])
//...
onMounted(async () => {
await Promise.all([
client.swr(request.value, api => {
results.value = api.response?.results || []
//...
}),
client.swr(new AnonData(), async api => {
topAlbums.value = api.response?.topAlbums || []
//...
}),
])
})
This results in UIs being immediately rendered on load and if the API response has changed, the updated reactive collections will re-render the UI with the updated data.
swrEffect​
The built-in swrEffect()
API uses Vue's watchEffect
to detect property changes to trigger invoking the API request and returning API responses
in an idiomatic ApiResult<T>
with a similarly pleasant declarative API without the unnecessary boilerplate:
const client = useClient()
//...
const api = client.swrEffect(() => new Hello({ name: props.name }))
It also includes a built-in debounce feature where you can collapse multiple event triggers within a specified duration (like input events when a user is typing), e.g. we can initiate an API request when a user has paused briefly after 50ms with:
const api = client.swrEffect(() => new Hello({ name: props.name }), { delayMs:50 })
TypeScript Definition​
TypeScript definition of the API surface area and type information for correct usage of useClient()
/** Maintain loading state whilst API Request is in transit */
const loading: Ref<boolean>
/** Maintain API Error in reactive Ref<ResponseStatus> */
const error: Ref<ResponseStatus>
/** Set error state with summary or field validation error */
function setError({ message, errorCode, fieldName, errors }: IResponseStatus);
/** Add field error to API error state */
function addFieldError({ fieldName, message, errorCode }: IResponseError);
/** Send a typed API request and return results in an ApiResult<TResponse> */
async function api<TResponse>(request:IReturn<TResponse> | ApiRequest, args?:any, method?:string);
/** Send a typed API request and return empty response in a void ApiResult */
async function apiVoid(request:IReturnVoid | ApiRequest, args?:any, method?:string);
/** Send a FormData API request and return results in an ApiResult<TResponse> */
async function apiForm<TResponse>(request:IReturn<TResponse> | ApiRequest, body:FormData, args?:any, method?:string);
/** Send a FormData API request and return empty response in a void ApiResult */
async function apiFormVoid(request: IReturnVoid | ApiRequest, body: FormData, args?: any, method?: string);