Progressive Enhancements
As discussed in ADR 0009, the move to server side rendered (SSR) charts will help support the minority of users who are unable to run JavaScript in their web browser. For those with JavaScript, components within the Web application rendered via Razor views may be enhanced for a value-added user experience.
Vue.js
Vue.js has been implemented within the Web project within the monorepo. By default it ships with Vite to manage the build process. It also comes with a dev server to help with rapid application development. To run the dev server, from within the web/src/Web.App folder run:
npm inpm run dev
To build the Vue components (as well as the other compiled assets) run:
npm run build
which will populate wwwroot with the desired outputs. The Web application may then be (re)started in order to pull in the latest resources. This stage is performed as part of the build pipelines.
Component basics
The Vue documentation is the best starting point for learning about the syntax and capabilities of the framework. The Vue Single-File Component convention should be used here, with each .vue file containing a <template>, <script> and optionally a <style> block. <template> defines what to render and <script> additional configuration and meta data related to that component. e.g.:
<!-- components/HelloWorld.Vue -->
<script setup lang="ts">
const { msg } = defineProps<{ msg: string; }>();
onMounted(() => {
console.log("message", msg);
});
</script>
<template>
<div></div>
</template>
defineProps() exposes the typed collection of properties to the <template>, as well as any consuming templates. It may also be destructured for use elsewhere within the setup block.
onMounted() is one of the common lifecycle functions that may be implemented if required, and in the example above writes the msg value to the console.
Basic state management is achieved via refs. Values may be mutated by setting the value property and explicit effects performed using watchEffect(). refs or other variables within the component’s context may also be bound to elements or other templates using v-bind:prop, or more commonly just :prop. e.g.:
<script setup lang="ts">
import MyButton from "./MyButton.vue"
const count = ref(0);
watchEffect(() => {
console.log(count.value));
}
</script>
<template>
<MyButton @click="count.value++" :count="count" class="my-class">
click(s) of the button
</MyButton>
</template>
<!--./MyButton.vue -->
<script setup lang="ts">
defineProps<{ count: number; }>();
</script>
<template>
<button>
<slot></slot>
</button>
</template>
In the example above, :count binds the ref count to the MyButton component props. The @click registers a click handler, which in this case increments the value of count. The child content may be rendered within the child component using <slot>s and the class is passed down as additional attributes that will render to the first element under the <template> in the child component (by default).
Unit tests
Vite components may be unit tested using Vitest by creating .spec.ts files in __tests__ folders alongside the component to be tested. e.g.:
// components/__tests__/HelloWorld.spec.ts
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import HelloWorld from "../HelloWorld.vue"; // as above
describe("HelloWorld", () => {
it("renders properly", () => {
const wrapper = mount(HelloWorld, { props: { msg: "Hello Vitest" } });
expect(wrapper.text()).toContain("Hello Vitest");
});
});
Other scripts
npm run formatto format all included files based on the prettier/eslint configurationnpm run test:unitto run the unit tests
IDE configuration
The following extensions are recommended for working with Vue in Visual Studio Code:
Mounting Vue.js components
The AssetSrc/ts/main.ts entry point exposes all of the components that should be bundled, as well as the createApp function. The project has been configured to build its output to dist/vite, which is copied to wwwroot/js when running gulp. This then allows the bundle to be imported directly into a Razor view, ideally as a partial within /web/src/Web.App/Views/Shared/Enhancements. e.g.:
<script type="module" add-nonce="true">
import {createApp, HelloWorld} from "@Html.FileVersionedPath("/js/main.js")";
const element = document.getElementById("#placeholder");
const helloWorld = createApp(HelloWorld, {
msg: "@(Model.Message)"
});
helloWorld.mount(element);
</script>
This partial should in turn be called from a scripts section within the parent view. e.g.:
<div id="placeholder"></div>
<!-- ... -->
@section scripts
{
@await Html.PartialAsync("Enhancements/_HelloWorld", new HelloWorldViewModel
{
Message = "Hello"
})
}
Styles
Component styling specific to progressive enhancements should be either added to enhancements.scss or maintained inline alongside each component itself. When migrating components from the front-end React project the source css files may be safely removed as both main.scss and enhancements.scss are built and included in wwwroot for consumption in the primary _Layout.cshtml view.
Debugging
When running the Vue dev server, the Vue DevTools are enabled by default. These may be used to mutate state to validate reactiveness, for example. The standard browser dev tools may also be used in conjunction with this. For a production build, source maps have been configured making it easy to step through .vue or other utility .ts files.