We're got a feature-packed JavaScript focus release that embraces JavaScript modules support in modern browsers to enable a simplified rapid development experience without the disadvantages that have plagued Single Page Apps (SPA) development up till now.
JavaScript Modules​
JavaScript modules have revolutionized the way we write and structure code in modern browsers which provide a number of advantages including better code organization and reuse, improved maintainability, and increased modularity and scalability which has seen it become a popular choice for building complex, scalable web applications.
With modules, developers can create small, focused pieces of code that can be easily imported and used in other parts of their application without needing to rely on any complex tooling. This results in a cleaner, more organized codebase that is easier to maintain over time.
Modern Vue.js Tailwind .NET Apps​
Our new templates builds upon JS Modules with a number of new integrated features to maximize productivity and performance for this revolutionary new approach to Web App development that offers new dramatically simplified and friction-free development experience without the need to rely on any heavy npm build tools - which we believe offers the best mix of productivity and simplicity available today.
Spearheaded by our exciting new @servicestack/vue Vue.js Tailwind Components, we've created a number of new Vue.js Tailwind project templates preconfigured with a minimal set of libraries to make you immediately productive out-of-the-box.
Vue.js Tailwind Templates
Vue.js Tailwind Live Demos​
All Razor Pages and MVC templates utilize the JS Modules support in modern browsers to avoid any needing any npm build system, for access to more advanced npm library features and to learn about Jamstack CDN and SSG benefits checkout Jamstack Templates docs.
To help choosing which template to start with, here's a quick summary of their differences:
- vue-mjs - Flagship Vue.mjs template complete with OrmLite, AutoQuery, boosted htmx links & static pre-rendered blogs
- razor - Simpler Razor Pages Template without a configured DB or static pre-rendered blogs
- mvc - Want to use MVC Identity Auth and Entity Framework
- web-tailwind - Empty tailwind template who don't want to use Razor Pages or MVC
- vue-vite - Want to use TypeScript in a simpler JAMStack Vite SPA App
- vue-ssg - Want to use TypeScript in an advanced JAMStack Vite SSG App
For a more in-depth look we've created an overview covering the differences between the Razor Pages & MVC templates:
Vue.js Bootstrap Razor Pages Template​
For devs preferring Bootstrap, we've also created a new Razor Pages template integrated with JS Modules:
Install with:
x new razor-pages MyApp
JS Modules Quick Look​
We'll take a quick glimpse on some of the power of JS Modules with the introduction of our new JS Module ES6 class DTOs feature, where you'll be able to walk up to any ServiceStack v6.6+ Web App, import some external modules independent from the rest of the page, then call end-to-end typed APIs without using any pre existing JS libraries or build tools in sight!
The Blazor Server live demo at blazor.web-templates.io is a good one to try this on given it's built on an entirely different server rendered technology stack. To try it out press F12
to open a dev console then paste in the sample code below:
document.body.insertAdjacentHTML('beforeend',`<div style="position:fixed;right:1em;top:5em">
<input type="text" id="txtName">
<div id="result"></div>
</div>`)
const ServiceStack = await import('https://unpkg.com/@servicestack/client@2/dist/servicestack-client.mjs')
const dtos = await import('/types/mjs')
const { JsonServiceClient, on, $1 } = ServiceStack
const client = new JsonServiceClient()
on('#txtName', {
async keyup(el) {
const api = await client.api(new dtos.Hello({ name:el.target.value }))
$1('#result').innerHTML = api.response.result
}
})
After the browser asynchronously loads the modules you should see a working text input which calls its Hello API on each key press:
Dynamically Loading Multiple JS Modules​
Ok that's cool, but let's see how far we can go with it and introduce the new Vue.js Tailwind Components into the mix to see how close we can get to recreating some of this Blazor Server App's functionality.
Lets 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": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js",
"@servicestack/client": "https://unpkg.com/@servicestack/client@2/dist/servicestack-client.min.mjs",
"@servicestack/vue": "https://unpkg.com/@servicestack/vue@3/dist/servicestack-vue.min.mjs"
}
});
document.body.appendChild(im)
const Vue = await import('vue')
const ServiceStack = await import('@servicestack/client')
const ServiceStackVue = await import('@servicestack/vue')
const dtos = await import('/types/mjs')
const BookingsApp = {
template:`
<div class="sm:max-w-fit p-4 m-4">
<h3 class="ml-4 text-center text-2xl font-medium">Vue.js Bookings</h3>
<AutoCreateForm v-if="create" type="CreateBooking" @done="done" @save="done" />
<AutoEditForm v-else-if="edit" type="UpdateBooking" deleteType="DeleteBooking" v-model="edit"
@done="done" @save="done" @delete="done" />
<OutlineButton @click="reset({ create:true })">New Booking</OutlineButton>
<DataGrid :items="bookings" type="Booking"
selected-columns="id,name,roomType,roomNumber,cost,bookingStartDate,bookingEndDate,couponId"
:header-titles="{roomNumber:'Room No',bookingStartDate:'Start Date',bookingEndDate:'End Date',couponId:'Voucher'}"
:visible-from="{ name:'xl', bookingStartDate:'sm', bookingEndDate:'xl', couponId:'xl' }"
@row-selected="editId = editId == $event.id ? null : $event.id" :is-selected="row => editId == row.id" />
</div>`,
setup(props) {
const { ref, onMounted, watch } = Vue
const { useClient, useAuth, useFormatters } = ServiceStackVue
const { QueryBookings } = dtos
const create = ref(false)
const editId = ref()
const edit = ref()
const bookings = ref([])
const client = useClient()
const { currency } = useFormatters()
async function refresh() {
const api = await client.api(new QueryBookings())
if (api.succeeded) {
bookings.value = api.response.results || []
}
}
onMounted(refresh)
function reset(args={}) {
create.value = args.create ?? false
editId.value = args.editId ?? undefined
}
function done() {
refresh()
reset()
}
watch(editId, async () => {
if (editId.value) {
const api = await client.api(new QueryBookings({ id: editId.value }))
if (api.succeeded) {
edit.value = api.response.results[0]
return
}
}
edit.value = null
})
return { create, editId, edit, bookings, reset, done, currency }
}
}
const app = Vue.createApp(BookingsApp)
app.provide('client', new ServiceStack.JsonServiceClient())
app.use(ServiceStackVue.default)
await ServiceStackVue.useMetadata().loadMetadata()
document.querySelector('main').insertAdjacentHTML('beforeend',`<div id="app"></div>`)
app.mount('#app')
Give it a moment to load all the modules and you should see a shiny new freshly baked Vue.js data grid infiltrating the Blazor Server App!
This isn't just cosmetic, it's a full CRUD Bookings App with responsive formatted DataGrid columns, API-enabled AutoForm components powered by the App's API Metadata with populated Enum drop downs, optimal number, date & checkbox inputs and validation binding:
It doesn't have all the features of the Blazor AutoQueryGrid component yet, but with a splash of code to load a few modules and create a custom Vue 3 reactive component, we can get most of the functionality without any of the heavy build tools complexity of a Blazor App or traditional npm SPA App and their matrix of dependencies.
Best mix of Productivity vs Simplicity​
From a pragmatic standpoint we believe JS Modules offers the best mix of productivity and simplicity, that can be progressively added to enhance server rendered Razor Pages with interactive reactive components without imposing its technical choices and bloating its other pages which are free to choose whichever libraries are best to implement its features.
Various WebApp DTO Options​
The only requirement for this to work is that the libraries are written as JS Modules which is a popular build target, but given we want to enable a typed and build-tools free future we've added a new .mjs Add ServiceStack Reference endpoint at /types/mjs to return your APIs DTOs in annotated ES6 class JS Module. To see where they benefit, lets quickly go over the existing options:
Common.js ES3 DTOs​
Our existing JavaScript Add ServiceStack Reference support returns your API DTOs in ES3 Common JS format, i.e. the default Target of TypeScript, in order to generate JS that's also compatible with older, out-dated browsers from 1999, that looks like:
var Hello = /** @class */ (function () {
function Hello(init) {
Object.assign(this, init);
}
Hello.prototype.getTypeName = function () { return 'Hello'; };
Hello.prototype.getMethod = function () { return 'POST'; };
Hello.prototype.createResponse = function () { return new HelloResponse(); };
return Hello;
}());
exports.Hello = Hello;
var HelloResponse = /** @class */ (function () {
function HelloResponse(init) {
Object.assign(this, init);
}
return HelloResponse;
}());
exports.HelloResponse = HelloResponse;
This does enable a similar build-free dev experience which lets you easily include your APIs DTOs in a page along with an Embedded UMD @servicestack/client to start making API calls, e.g:
<script src="/js/require.js"></script>
<script src="/js/servicestack-client.js"></script>
<script src="/types/js"></script>
<script>
var { JsonServiceClient, Hello } = exports
var client = new JsonServiceClient()
function callHello(name) {
client.get(new Hello({ name }))
.then(function(r) {
document.getElementById('result').innerHTML = r.result
});
}
</script>
But it offers no type-checking or intelli-sense assistance during development, luckily we can enable static analysis support by including TypeScript dtos.ts in the same project which smart IDEs like JetBrains Rider will find to light up assistance.
TypeScript DTOs​
The TypeScript DTOs capture the most Type Information about your API DTOs in generic TypeScript classes:
// @Route("/hello")
// @Route("/hello/{Name}")
export class Hello implements IReturn<HelloResponse>
{
public name?: string;
public constructor(init?: Partial<Hello>) { (Object as any).assign(this, init); }
public getTypeName() { return 'Hello'; }
public getMethod() { return 'POST'; }
public createResponse() { return new HelloResponse(); }
}
export class HelloResponse
{
public result: string;
public constructor(init?: Partial<HelloResponse>) { (Object as any).assign(this, init); }
}
Which is still the best option to use in our JAMStack TypeScript templates where the DTOs integrate with the rest of your App's TypeScript code-base and its npm build tools takes care to transform it into its configured downlevel JS target bundles.
The primary issue being that Browsers can't run them natively, so developing in TypeScript typically requires an npm build system to do the transformation which adds complexity and results in slower iterative dev cycles.
JS Module ES6 class DTOs​
The new ES6 classes combines the best of both worlds to enable a productive type-safe development model during development but can also be referenced as-is in JS Module scripts and run natively in browsers without any build tools!
To achieve this the ES6 classes are annotated with JSDoc type hints in comments which enjoys broad support in IDEs and tools like TypeScript where it can be used to provide type information in JavaScript files, which looks like:
export class Hello {
/** @param {‎{name?:string}‎} [init] */
constructor(init) { Object.assign(this, init) }
/** @type {string} */
name;
getTypeName() { return 'Hello' }
getMethod() { return 'GET' }
createResponse() { return new HelloResponse() }
}
export class HelloResponse {
/** @param {‎{result?:string,responseStatus?:ResponseStatus}‎} [init] */
constructor(init) { Object.assign(this, init) }
/** @type {string} */
result;
/** @type {?ResponseStatus} */
responseStatus;
}
Which our JS Apps can immediately use by referencing the /types/mjs endpoint directly:
import { Hello } from '/types/mjs'
const api = await client.api(new Hello({ name }))
That for better IDE intelli-sense during development, we can save to disk with:
npm run dtos
Where it enables IDE static analysis when calling Typed APIs from JavaScript:
import { Hello } from '/mjs/dtos.mjs'
.mjs Add ServiceStack Reference​
The new JS Modules DTOs is now a first-class Add ServiceStack Reference language supported language complete with IDE integration in the latest VS .NET and JetBrains IDEs extensions:
Including support in the latest dotnet tool for creating new .mjs Add ServiceStack References:
$ x mjs https://localhost:5001
And updating them:
x mjs
But the best thing about JS Module DTOs is using them in the exciting new node_modules free world enabled in the new Vue.js templates!
Multi Page Apps​
Back to Multi Page Apps
Discover the productive build tools free world of modern JS Module MPAs
JavaScript has progressed significantly in recent times where many of the tooling & language enhancements that we used to rely on external tools for is now available in modern browsers alleviating the need for complex tooling and npm dependencies that have historically plagued modern web development.
The good news is that the complex npm tooling that was previously considered mandatory in modern JavaScript App development can be considered optional as we can now utilize modern browser features like async/await, JavaScript Modules, dynamic imports, import maps and modern language features for a sophisticated development workflow without the need for any npm build tools.
Bringing Simplicity Back​
The new Razor Vue.mjs templates focuses on simplicity and eschews many aspects that has complicated modern JavaScript development, specifically:
- No npm node_modules or build tools
- No client side routing
- No heavy client state
Effectively abandoning the traditional SPA approach in lieu of a simpler MPA development model using Razor Pages for Server Rendered content with any interactive UIs progressively enhanced with JavaScript.
Freedom to use any JS library​
Avoiding the SPA route ends up affording more flexibility on which JS libraries each page can use as without heavy bundled JS blobs of all JS used in the entire App, it's free to only load the required JS each page needs to best implement its required functionality, which can be any JS library, preferably utilizing ESM builds that can be referenced from a JavaScript Module, taking advantage of the module system native to modern browsers able to efficiently download the declarative matrix of dependencies each script needs.
Best libraries for progressive Multi Page Apps​
By default the Razor Vue.js templates includes a collection of libraries we believe offers the best modern development experience in Progressive MPA Web Apps, specifically:
Tailwind​
Tailwind CLI enables a responsive, utility-first CSS framework for creating maintainable CSS at scale without the need for any CSS preprocessors like Sass, which is configured to run from an npx script to avoid needing any node_module dependencies.
Vue 3​
Vue is a popular Progressive JavaScript Framework that makes it easy to create interactive Reactive Components whose Composition API offers a nice development model without requiring any pre-processors like JSX.
Where creating a component is as simple as:
const Hello = {
template: `<b>Hello, {‎{name}‎}!</b>`,
props: { name:String }
}
Or a simple reactive example:
import { ref } from "vue"
const Counter = {
template: `<b @click="count++">Counter {‎{count}‎}</b>`,
setup() {
let count = ref(1)
return { count }
}
}
These components can be mounted using the standard Vue 3 mount API, but to
make it easier we've added additional APIs for declaratively mounting components to pages using the data-component
and data-props
attributes, especially useful for embedding Vue components in Markdown content, e.g:
<div data-component="Hello" data-props="{ name: 'Vue 3' }"></div>
Alternatively they can be programmatically added using the custom mount
method in api.mjs
:
import { mount } from "/mjs/api.mjs"
mount('#counter', Counter)
Both methods create components with access to all your Shared Components and any 3rd Party Plugins which we can preview in this example that uses @servicestack/vue's PrimaryButton and ModalDialog components:
const Plugin = {
template:`<div>
<PrimaryButton @click="show=true">Open Modal</PrimaryButton>
<ModalDialog v-if="show" @done="show=false">
<div class="p-8">Hello @servicestack/vue!</div>
</ModalDialog>
</div>`,
setup() {
const show = ref(false)
return { show }
}
}
Vue.js Tailwind Components Library
@servicestack/vue is our growing Vue 3 Tailwind component library with a number of rich Tailwind components useful in .NET Web Apps, including DataGrids, Auto Forms and Input Components with integrated contextual validation binding.
For a quick preview of the fast dev workflow of using these components in a Razor Pages App, checkout:
@servicestack/vue is our cornerstone library for enabling a highly productive dev model across our Vue.js Tailwind Project templates that we'll be continuing to invest in to build a richer component library unlocking greater productivity - watch this space!
@servicestack/client​
@servicestack/client is our generic JS/TypeScript client library which enables a terse, typed API for using your App's typed DTOs from the built-in JavaScript ES6 Classes support to enable an effortless end-to-end Typed development model for calling your APIs without any build steps, e.g:
<input type="text" id="txtName">
<div id="result"></div>
<script type="module">
import { JsonServiceClient, $1, on } from '@servicestack/client'
import { Hello } from '/types/mjs'
on('#txtName', {
async keyup(el) {
const client = new JsonServiceClient()
const api = await client.api(new Hello({ name:el.target.value }))
$1('#result').innerHTML = api.response.result
}
})
</script>
For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with:
npm run dtos
That can be referenced instead to unlock your IDE's static analysis type-checking and intelli-sense benefits during development:
import { Hello } from '/js/dtos.mjs'
client.api(new Hello({ name }))
You'll typically use all these libraries in your API-enabled components as seen in the HelloApi.mjs component on the home page which calls its Hello API on each key press:
import { ref } from "vue"
import { useClient } from "@servicestack/vue"
import { Hello } from "../dtos.mjs"
export default {
template:/*html*/`<div class="flex flex-wrap justify-center">
<TextInput v-model="name" @keyup="update" />
<div class="ml-3 mt-2 text-lg">{‎{ result }‎}</div>
</div>`,
props:['value'],
setup(props) {
let name = ref(props.value)
let result = ref('')
let client = useClient()
async function update() {
let api = await client.api(new Hello({ name }))
if (api.succeeded) {
result.value = api.response.result
}
}
update()
return { name, update, result }
}
}
We'll also go through and explain other features used in this component:
/*html*/
​
Although not needed in Rider (which can automatically infer HTML in strings), the /*html*/
type hint is used to instruct tooling like the es6-string-html
VS Code extension to provide syntax highlighting and an enhanced authoring experience for HTML content in strings.
useClient​
useClient() provides managed APIs around the JsonServiceClient
instance, registered in Vue App's with:
let client = new JsonServiceClient()
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 includes:
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()
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:
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 /TodoMvc:
<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 to display any field validation errors adjacent to the HTML Input
with matching id
fields:
let store = {
/** @type {Todo[]} */
todos: [],
newTodo:'',
error:null,
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 = ''
else
this.error = 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 }
}
}
Effectively making 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:
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 you want to create the form of, 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, including:
Form Input Components​
In addition to including Tailwind versions of the standard HTML Form Inputs controls to create beautiful Tailwind Forms, it also contains a variety of integrated high-level components:
FileInput​
FileInput component beautifies the browsers default HTML file Input, supporting both Single file uploads:
<FileInput id="profileUrl" label="Single File Upload" v-model="contact.profileUrl" />
and Multiple File Uploads:
<FileInput id="profileUrls" label="Multiple File Uploads" multiple :files="contact.files" />
Invoking APIs containing uploaded files​
When uploading files, you'll need to submit API requests using the apiForm
or apiFormVoid
methods to send
a populated FormData
instead of a Request DTO, e.g:
<form @submit.prevent="submit">
<FileInput id="profileUrls" label="Multiple File Uploads" multiple :files="files" />
<PrimaryButton>Save</PrimaryButton>
</form>
<script setup lang="ts">
import { useClient } from "@servicestack/vue"
import { CreateContact } from "/mjs/dtos.mjs"
const client = useClient()
async function submit(e:Event) {
const form = e.target as HTMLFormElement
const api = await client.apiForm(new CreateContact(), new FormData(form))
if (api.succeeded) {
//...
}
}
</script>
Integrates with Managed File Uploads​
Using Managed File Uploads is a productive solution for easily managing file uploads where you can declaratively specify which location uploaded files should be written to, e.g:
public class UpdateContact : IPatchDb<Contact>, IReturn<Contact>
{
public int Id { get; set; }
[ValidateNotEmpty]
public string? FirstName { get; set; }
[ValidateNotEmpty]
public string? LastName { get; set; }
[Input(Type = "file"), UploadTo("profiles")]
public string? ProfileUrl { get; set; }
public int? SalaryExpectation { get; set; }
[ValidateNotEmpty]
public string? JobType { get; set; }
public int? AvailabilityWeeks { get; set; }
public EmploymentType? PreferredWorkType { get; set; }
public string? PreferredLocation { get; set; }
[ValidateNotEmpty]
public string? Email { get; set; }
public string? Phone { get; set; }
[Input(Type = "tag"), FieldCss(Field = "col-span-12")]
public List<string>? Skills { get; set; }
[Input(Type = "textarea")]
[FieldCss(Field = "col-span-12 text-center", Input = "h-48", Label= "text-xl text-indigo-700")]
public string? About { get; set; }
}
This metadata information is also available to AutoForm components which supports invoking APIs with uploaded files:
<AutoEditForm type="UpdateContact" v-model="contact" formStyle="card" />
TagInput​
TagInput component provides a user friendly control for managing a free-form List<string>
tags or symbols
which is also supported in declarative Auto Forms using the [Input(Type="tag")]
attribute as seen in the
UpdateContact example above using the AutoForm components.
Alternatively <TagInput>
can be used in Custom Forms directly by binding to a List<string>
or string[]
model:
<form @submit.prevent="submit">
<div class="shadow sm:rounded-md bg-white dark:bg-black">
<div class="relative px-4 py-5 sm:p-6">
<fieldset>
<legend class="text-base font-medium text-gray-900 dark:text-gray-100 text-center mb-4">
TagInput Examples
</legend>
<ErrorSummary :except="visibleFields" />
<div class="grid grid-cols-12 gap-6">
<div class="col-span-6">
<TextInput v-model="request.firstName" />
</div>
<div class="col-span-6">
<TextInput v-model="request.lastName" />
</div>
<div class="col-span-12">
<TagInput v-model="request.skills" label="Technology Skills" />
</div>
</div>
</fieldset>
</div>
<div class="mt-4 px-4 py-3 bg-gray-50 dark:bg-gray-900 sm:px-6 flex flex-wrap justify-between">
<div></div>
<div class="flex justify-end">
<SecondaryButton class="mr-4">Cancel</SecondaryButton>
<PrimaryButton type="submit">Submit</PrimaryButton>
</div>
</div>
</div>
</form>
Autocomplete​
Autocomplete component provides a user friendly Input for being able to search and quickly select items with support for partial items view and infinite scrolling.
useAuth​
Your Vue.js code can access Authenticated Users using useAuth()
which can also be populated without the overhead of an Ajax request by embedding the response of the built-in
Authenticate API inside _Layout.cshtml
with:
<script type="module">
import { useAuth } from "@@servicestack/vue"
const { signIn } = useAuth()
signIn(@await Html.ApiAsJsonAsync(new Authenticate()))
</script>
Where it enables access to the below useAuth() utils for inspecting the current authenticated user:
const {
signIn, // Sign In the currently Authenticated User
signOut, // Sign Out currently Authenticated User
user, // Access Authenticated User info in a reactive Ref<AuthenticateResponse>
isAuthenticated, // Check if the current user is Authenticated in a reactive Ref<boolean>
hasRole, // Check if the Authenticated User has a specific role
hasPermission, // Check if the Authenticated User has a specific permission
isAdmin // Check if the Authenticated User has the Admin role
} = useAuth()
This is used in Bookings.mjs
to control whether the <AutoEditForm>
component should enable its delete functionality:
export default {
template/*html*/:`
<AutoEditForm type="UpdateBooking" :deleteType="canDelete ? 'DeleteBooking' : null" />
`,
setup(props) {
const { hasRole } = useAuth()
const canDelete = computed(() => hasRole('Manager'))
return { canDelete }
}
}
JSDoc​
We get great value from using TypeScript to maintain our libraries typed code bases, however it does mandate using an external tool to convert it to valid JS before it can be run, something the new Razor Vue.js templates expressly avoids.
Instead it adds JSDoc type annotations to code where it adds value, which at the cost of slightly more verbose syntax enables much of the same static analysis and intelli-sense benefits of TypeScript, but without needing any tools to convert it to valid JavaScript, e.g:
/** @param {KeyboardEvent} e */
function validateSafeName(e) {
if (e.key.match(/[\W]+/g)) {
e.preventDefault()
return false
}
}
TypeScript static analysis during development​
Whilst the code-base doesn't use TypeScript syntax in its code base directly, it still benefits from TypeScript's language services in IDEs for the included libraries from the TypeScript definitions included in /lib/typings
, downloaded in
postinstall.js after npm install.
Import Maps​
Import Maps is a useful browser feature that allows specifying optimal names for modules, that can be used to map package names to the implementation it should use, e.g:
@Html.StaticImportMap(new() {
["vue"] = "/lib/mjs/vue.mjs",
["@servicestack/client"] = "/lib/mjs/servicestack-client.mjs",
["@servicestack/vue"] = "/lib/mjs/servicestack-vue.mjs",
})
Where they can be freely maintained in one place without needing to update any source code references. This allows source code to be able to import from the package name instead of its physical location:
import { ref } from "vue"
import { useClient } from "@servicestack/vue"
import { JsonApiClient, $1, on } from "@servicestack/client"
It's a great solution for specifying using local unminified debug builds during Development, and more optimal CDN hosted production builds when running in Production, alleviating the need to rely on complex build tools to perform this code transformation for us:
@Html.ImportMap(new()
{
["vue"] = ("/lib/mjs/vue.mjs", "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"),
["@servicestack/client"] = ("/lib/mjs/servicestack-client.mjs", "https://unpkg.com/@servicestack/client@2/dist/servicestack-client.min.mjs"),
["@servicestack/vue"] = ("/lib/mjs/servicestack-vue.mjs", "https://unpkg.com/@servicestack/vue@3/dist/servicestack-vue.min.mjs")
})
Note: Specifying exact versions of each dependency improves initial load times by eliminating latency from redirects.
Or if you don't want to reference any external dependencies, have the ImportMap reference local minified production builds instead:
@Html.ImportMap(new()
{
["vue"] = ("/lib/mjs/vue.mjs", "/lib/mjs/vue.min.mjs"),
["@servicestack/client"] = ("/lib/mjs/servicestack-client.mjs", "/lib/mjs/servicestack-client.min.mjs"),
["@servicestack/vue"] = ("/lib/mjs/servicestack-vue.mjs", "/lib/mjs/servicestack-vue.min.mjs")
})
Polyfill for Safari​
Unfortunately Safari is the last modern browser to support import maps which is only now in Technical Preview. Luckily this feature can be polyfilled with the pre-configured ES Module Shims:
@if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome")))
{
<script async src="https://ga.jspm.io/npm:es-module-shims@1.6.3/dist/es-module-shims.js"></script>
}
Fast Component Loading​
SPAs are notorious for being slow to load due to needing to download large blobs of JavaScript bundles that it needs to initialize with their JS framework to mount their App component before it starts fetching the data from the server it needs to render its components.
A complex solution to this problem is to server render the initial HTML content then re-render it again on the client after the page loads. A simpler solution is to avoid unnecessary ajax calls by embedding the JSON data the component needs in the page that loads it, which is what /TodosMvc does to load its initial list of todos using the Service Gateway to invoke APIs in process and embed its JSON response with:
<script>todos = @await ApiResultsAsJsonAsync(new QueryTodos())</script>
<script type="module">
import TodoMvc from "/Pages/TodoMvc.mjs"
import { mount } from "/mjs/app.mjs"
mount('#todomvc', TodoMvc, { todos })
</script>
Where ApiResultsAsJsonAsync
is a simplified helper that uses the Gateway to call your API and returns its unencoded JSON response:
(await Gateway.ApiAsync(new QueryTodos())).Response?.Results.AsRawJson();
The result of which should render the List of Todos instantly when the page loads since it doesn't need to perform any additional Ajax requests after the component is loaded.
Fast Page Loading​
We can get SPA-like page loading performance using htmx's Boosting feature which avoids full page reloads
by converting all anchor tags to use Ajax to load page content into the page body, improving performance from avoiding needing to reload
scripts and CSS in <head>
This is used in Header.cshtml to boost all main navigation links:
<nav hx-boost="true">
<ul>
<li><a href="/Blog">Blog</a></li>
</ul>
</nav>
htmlx has lots of useful real world examples that can be activated with declarative attributes, another feature the vue-mjs template uses is the class-tools extension to hide elements from appearing until after the page is loaded:
<div id="signin"></div>
<div class="hidden mt-5 flex justify-center" classes="remove hidden:load">
@Html.SrcPage("SignIn.mjs")
</div>
Which reduces UI yank from not showing server rendered content before JS components have loaded.
Fast pre-rendered static generated Razor Pages​
Whilst not required, the vue-mjs template also includes support for pre-rendering static content from Razor Pages.
Prerendering static content is a popular technique used by JAMStack Apps to improve the performance, reliability and scalability of Web Apps that's able to save unnecessary computation at runtime by generating static content at deployment which can be optionally hosted from a CDN for even greater performance.
As such we thought it a valuable technique to include the vue-mjs template to show how it can be easily achieved within a Razor Pages App. Since prerendered content is only updated at deployment, it's primarily only useful for static content like this Blog which is powered by the static markdown content in _blog/posts whose content is prerendered to /wwwroot/blog
.
For those interested in utilizing this optimization we've published details on how this works in the Prerendering Razor Pages blog post.
Develop using JetBrains Rider​
Given it's best-of-class support for Web Development we recommend using JetBrains Rider for any kind of JS or TypeScript development. If you're using Rider checkout the Develop using JetBrains Rider blog post for an optimal setup for Vue.js and Tailwind Web Apps.
Develop using Visual Studio​
If you prefer using VS .NET we recommend using VS Code for all your Apps JS front-end development and optionally VS .NET for the back-end C#/.NET development of large .NET Projects. If using VS checkout Develop using Visual Studio blog post for an optimal setup for utilizing Vue.js composition API and Tailwind.
Feedback Welcome!​
We hope you enjoy these exciting new templates in this release, as always if you have any questions or feedback in this release please let us know in ServiceStack/Discuss GitHub Discussions or the Customer Forums.
@servicestack/client now dependency-free​
Now that fetch has finally landed in Node.js v18+ LTS we've gone ahead and removed all polyfills to make @servicestack/client dependency-free in its latest major v2.x version!
This should have no effect when using JsonServiceClient in Browsers which uses its native fetch()
or from Node.js v18+ that now has native fetch
support as well.
ServerEventsClient in Node.js​
But Node.js projects using ServerEventsClient (e.g. in tests) now require a polyfill:
npm install eventsource
Then polyfill with:
globalThis.EventSource = require("eventsource")
JsonServiceClient in Node.js < v18​
Older Node.js runtimes using JsonServiceClient
can continue using the existing v1.x version or polyfill fetch
with:
npm install cross-fetch
Then polyfill with:
require('cross-fetch/polyfill')
MSVR 76883​
We've received a vulnerability report from the Microsoft Vulnerability Research team last week who found a potential vulnerability in ServiceStack.Redis (.NET Framework) if an attack is able to write a malicious string into a Redis Server and trick an Application into reading it back into a C# DTO with a nested complex type containing a late-bound object
property, the report reads:
This attack requires a malicious string to be written to the Redis cache and then read back as an object. The most likely attack pattern here is going to involve some sort of injection attack where an application can be tricked into writing untrusted data to a Redis cache that it will later read back.
Unfortunately it's another example of exploiting the same issue that's plagued .NET Serializers for years where the existence of dangerous classes in .NET Framework where setting a public instance property can cause an App to load and execute code in an external .dll requires all .NET Serializers supporting dynamic payloads to maintain a Runtime Type Whitelist of Types that are allowed to be dynamically instantiated in object
properties.
This vulnerability found a nested structure code path which skipped the whitelist checks which has since been resolved in this release.
As a result existing code deserializing non-whitelisted unverified types in late-bound object
properties will start throwing NotSupportedException
, to resolve, Types needs to be allowed in the Runtime Type Whitelist which by default can be annotated with:
[Serializable]
[DataContract]
[RuntimeSerializable]
Or implement one of these interfaces:
ISerializable
IConvertible
IRuntimeSerializable
IReturn<T>
IReturnVoid
IMeta
IVerb // IGet, IPost, IPut, IPatch, etc
ICrud // ICreateDb`1, IUpdateDb`1, etc
IAuthTokens
IHasResponseStatus
IHasId<T>
Effectively it looks to allow any serializable models and DTOs it can find by looking at all available heuristics.