# What is Razor Press? Source: https://razor-press.web-templates.io/what-is-razor-press Razor Press is a Razor Pages powered Markdown alternative to Ruby's Jekyll, Vue & VitePress that's ideal for generating fast, static content-centric & documentation websites. Inspired by [VitePress](https://vitepress.dev), it's designed to effortlessly create documentation around content written in Markdown, rendered using C# Razor Pages and beautifully styled with [tailwindcss](https://tailwindcss.com) and [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin). The resulting statically generated HTML pages can easily be deployed anywhere, where it can be hosted by any HTTP Server or CDN. By default it includes GitHub Actions to deploy it your GitHub Repo's **gh-pages** branch where it's hosted for FREE on [GitHub Pages](https://pages.github.com) CDN which can be easily configured to use your [Custom Domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site). ## Use Cases Razor Press utilizes the same technology as [Razor SSG](https://razor-ssg.web-templates.io/posts/razor-ssg) which is the template we recommend for developing any statically generated sites with Razor like Blogs, Portfolios, and Marketing Sites as it includes more Razor & Markdown features like blogs and integration with [Creator Kit](https://servicestack.net/creatorkit/) - a companion OSS project offers the necessary tools any static website can use to reach and retain users, from managing subscriber mailing lists to moderating a feature-rich comments system. Some examples built with Razor SSG include:
## Documentation Razor Press is instead optimized for creating documentation and content-centric websites, with built-in features useful for documentation websites including: - Customizable Sidebar Menus - Document Maps - Document Page Navigation - Autolink Headers #### Markdown Extensions - Markdown Content Includes - Tip, Info, Warning, Danger sections - Copy and Shell command widgets But given **Razor Press** and **Razor SSG** share the same implementation, their features are easily transferable, e.g. The [What's New](/whatsnew) and [Videos](/videos) sections are [features copied](https://razor-ssg.web-templates.io/posts/razor-ssg#whats-new-feature) from Razor SSG as they can be useful in Documentation websites. ## Customizable {#custom-anchor .custom} The source code of all Markdown and Razor Pages features are included in the template with all Markdown extensions implemented in the [Markdown*.cs](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp) files allowing for easier inspection, debugging and customization. To simplify updating Markdown features in future we recommend against modifying the included `Markdown.*` files and instead add any Markdig pipeline extensions or custom containers using `MarkdigConfig` in `Configure.Ssg.cs`: ```csharp MarkdigConfig.Set(new MarkdigConfig { ConfigurePipeline = pipeline => { // Extend Markdig Pipeline }, ConfigureContainers = config => { config.AddBuiltInContainers(); // Add Custom Block or Inline containers } }); ``` ### Update Markdown Extensions & Dependencies Updating to the latest JavaScript dependencies and Markdown extensions can be done by running: :::sh npm install ::: Which as the template has no npm dependencies, is just an alias for running `node postinstall.js` ## Example The largest website generated with Razor Press is currently the ServiceStack's documentation at [docs.servicestack.net](https://docs.servicestack.net): A **500+** pages documentation website ported from VitePress, which prompted the creation of Razor Press after experiencing issues with VitePress's SSR/SPA model whose workaround became too time consuming to maintain. The new Razor SSG implementation now benefits from Razor Pages flexible layouts and partials where pages can be optionally implemented in just markdown, Razor or a hybrid mix of both. The [Vue](/vue/) splash page is an example of this implemented in a custom [/Vue/Index.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Vue/Index.cshtml) Razor Page. ## Feedback & Feature Requests Welcome Up to this stage [docs.servicestack.net](https://docs.servicestack.net) has been the primary driver for Razor Press current feature-set, re-implementing all the previous VitePress features it used with C#, Razor Pages and Markdig extensions. In future we'll look at expanding this template with generic Markdown features suitable for documentation or content-centric websites, for which we welcome any feedback or new feature requests at: # Structure Source: https://razor-press.web-templates.io/structure ### Markdown Feature Structure All markdown features are effectively implemented in the same way, starting with a **_folder** for maintaining its static markdown content, a **.cs** class to load the markdown and a **.cshtml** Razor Page to render it: | Location | Description | |------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| | `/_{Feature}` | Maintains the static markdown for the feature | | `Markdown.{Feature}.cs` | Functionality to read the feature's markdown into logical collections | | `{Feature}.cshtml` | Functionality to Render the feature | | [Configure.Ssg.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Configure.Ssg.cs) | Initializes and registers the feature with ASP .NET's IOC | Lets see what this looks like in practice by walking through the "Pages" feature: ## Pages Feature The pages feature simply makes all pages in the **_pages** folder, available from `/{filename}`. Where the included pages: ```files /_pages what-is-razor-press.md structure.md privacy.md ``` Are made available from: - [/what-is-razor-press](/what-is-razor-press) - [/structure](/structure) - [/privacy](/privacy) This is primarily where most Markdown documentation will be maintained. ### Document Collections Folders can be used to maintain different document collections as seen in [/vue/](/vue/) and [/creatorkit/](/creatorkit/) folders: ```files /_pages /creatorkit about.md components.md customize.md /vue alerts.md autocomplete.md autoform.md ``` Each documentation collection needs a Razor Page to render each page in that collection, which can be configured independently and include additional features when needed, examples of this include: - [/Vue/Page.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/Vue/Page.cshtml) - [/CreatorKit/Page.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/CreatorKit/Page.cshtml) They can contain custom Razor Pages as needed, e.g. both [/vue/](/and/) and [/creatorkit/](/creatorkit/) have custom index pages: - [/Vue/Index.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/Vue/Index.cshtml) - [/CreatorKit/Index.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/CreatorKit/Index.cshtml) If no custom home page is needed, a `/{slug?}` or `/{**slug}` wildcard route can be used to handle a collection's index and content pages, e.g: - [/AutoQuery.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/AutoQuery.cshtml) - [/OrmLite.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/OrmLite.cshtml) - [/Redis.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/Redis.cshtml) Which are used to render all pages in each documentation collection: - [docs.servicestack.net/autoquery/](https://docs.servicestack.net/autoquery/) - [docs.servicestack.net/ormlite/](https://docs.servicestack.net/ormlite/) - [docs.servicestack.net/redis/](https://docs.servicestack.net/redis/) ::: tip See [Sidebars](/sidebars) for how to configure different Sidebar menus for each collection ::: ### Loading Markdown Pages The code that loads the Pages feature markdown content is in [Markdown.Pages.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Markdown.Pages.cs), which ultimately just loads Markdown files using the configured [Markdig](https://github.com/xoofx/markdig) pipeline that is made available via its `VisiblePages` property which returns all documents **during development** but hides any **Draft** or content published at a **Future Date** from **production builds**. ## What's New Feature The [/whatsnew](/whatsnew) page is an example of creating a custom Markdown feature to implement a portfolio or a product releases page where a new folder is created per release, containing both release date and release or project name, with all features in that release maintained markdown content sorted in alphabetical order: ```files /_whatsnew /2023-03-08_Animaginary feature1.md /2023-03-18_OpenShuttle feature1.md /2023-03-28_Planetaria feature1.md ``` What's New follows the same structure as Pages feature which is loaded in: - [Markdown.WhatsNew.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Markdown.WhatsNew.cs) and rendered in: - [WhatsNew.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/WhatsNew.cshtml) ## Markdown Videos Feature Videos is another Markdown powered feature for display collections of YouTube videos populated from a Directory of Markdown Video pages in [/_videos](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/_videos): ```files /_videos /projects video1.md video2.md /vue video1.md video2.md ``` Loaded with: - [Markdown.Videos.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Markdown.Videos.cs) and Rendered with Razor Pages: - [Shared/VideoGroup.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Shared/VideoGroup.cshtml) - Razor Partial for displaying a Video Collection - [Videos.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Videos.cshtml) - Razor Page displaying multiple Video Collections ## Metadata APIs Feature Typically a disadvantage of statically generated websites is the lack of having APIs we can call to query website data in a easily readable data format like JSON. However we can also easily support this by also pre-rendering static `*.json` data structures along with the pre-rendered website at deployment. This capability is provided by the new [Markdown.Meta.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Markdown.Meta.cs) feature which generates multiple projections of the Markdown metadata for each type of content added in every year, e.g: ```files /meta /2021 all.json posts.json videos.json /2022 all.json posts.json /2023 all.json pages.json posts.json videos.json whatsnew.json all.json index.json ``` With this you can fetch the metadata of all the new **Blog Posts** added in **2023** from: [/2023/posts.json](https://razor-ssg.web-templates.io/meta/2023/posts.json) Or all the website content added in **2023** from: [/2023/all.json](https://razor-ssg.web-templates.io/meta/2023/all.json) Or **ALL** the website metadata content from: [/all.json](https://razor-ssg.web-templates.io/meta/all.json) This feature makes it possible to support use-cases like CreatorKit's [Generating Newsletters](https://servicestack.net/creatorkit/portal-mailruns#generating-newsletters) feature which generates a Monthly Newsletter Email with all new content added within a specified period. ## General Features Most unique markdown features are captured in their Markdown's frontmatter metadata, but in general these features are broadly available for all features: - **Live Reload** - Latest Markdown content is displayed during **Development** - **Custom Layouts** - Render post in custom Razor Layout with `layout: _LayoutAlt` - **Drafts** - Prevent posts being worked on from being published with `draft: true` - **Future Dates** - Posts with a future date wont be published until that date - **Order** - Specify custom ordering for a collection pages ### Initializing and Loading Markdown Features All markdown features are initialized in the same way in [Configure.Ssg.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Configure.Ssg.cs) where they're registered in ASP.NET Core's IOC and initialized after the App's plugins are loaded by injecting with the App's [Virtual Files provider](https://docs.servicestack.net/virtual-file-system) before using it to read from the directory where the markdown content for each feature is maintained: ```csharp public class ConfigureSsg : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { context.Configuration.GetSection(nameof(AppConfig)).Bind(AppConfig.Instance); services.AddSingleton(AppConfig.Instance); services.AddSingleton` element instead of it's default markdown rendering: ```markdown :::pre ... ::: ``` ### copy The **copy** container is ideal for displaying text snippets in a component that allows for easy copying: #### Input ```markdown :::copy Copy Me! ::: ``` #### Output :::copy Copy Me! ::: HTML or XML fragments can also be copied by escaping them first: #### Input ```markdown :::copy `` ::: ``` #### Output :::copy ` ` ::: ### sh Similarly the **sh** container is ideal for displaying and copying shell commands: #### Input ```markdown :::sh npm run prerender ::: ``` #### Output :::sh npm run prerender ::: ## Implementing Block Containers [Markdig Containers](https://github.com/xoofx/markdig/blob/master/src/Markdig.Tests/Specs/CustomContainerSpecs.md) are a great way to create rich widgets that can be used directly in Markdown. They're useful for ensuring similar content is displayed consistently across all your documentation. A good use-case for this could be to implement a YouTube component for standardizing how YouTube videos are displayed. For this example we want to display a YouTube video using just its YouTube **id** and a **title** for the video which we can capture in the Custom Container: ```markdown :::YouTube MRQMBrXi5Sc Using Razor SSG to Create Websites in GitHub Codespaces ::: ``` Which we can implement with a normal Markdig `HtmlObjectRenderer `: ```csharp public class YouTubeContainer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, CustomContainer obj) { if (obj.Arguments == null) { renderer.WriteLine($"Missing YouTube Id, Usage :::{obj.Info} "); return; } renderer.EnsureLine(); var youtubeId = obj.Arguments!; var attrs = obj.TryGetAttributes()!; attrs.Classes ??= new(); attrs.Classes.Add("not-prose text-center"); renderer.Write(" '); renderer.WriteLine(""); } } ``` That should be registered in `Configure.Ssg.cs` with the name we want to use for the container: ```csharp MarkdigConfig.Set(new MarkdigConfig { ConfigureContainers = config => { // Add Custom Block or Inline containers config.AddBlockContainer("YouTube", new YouTubeContainer()); } }); ``` After which it can be used in your Markdown documentation: #### Input ```markdown :::YouTube MRQMBrXi5Sc Using Razor SSG to Create Websites in GitHub Codespaces ::: ``` #### Output :::YouTube MRQMBrXi5Sc Using Razor SSG to Create Websites in GitHub Codespaces ::: ### Custom Attributes Since we use `WriteAttributes(obj)` to emit any attributes we're also able to customize the widget to use a custom **id** and classes, e.g: #### Input ```markdown :::YouTube MRQMBrXi5Sc {.text-indigo-600} Using Razor SSG to Create Websites in GitHub Codespaces ::: ``` #### Output :::YouTube MRQMBrXi5Sc {.text-indigo-600} Using Razor SSG to Create Websites in GitHub Codespaces ::: ## Implementing Inline Containers Custom Inline Containers are useful when you don't need a to capture a block of content, like if we just want to display a video without a title, e.g: ```markdown ::YouTube MRQMBrXi5Sc:: ``` Inline Containers can be implemented with a Markdig `HtmlObjectRenderer"); renderer.WriteChildren(obj); renderer.WriteLine(""); renderer.WriteLine(@$"`, e.g: ```csharp public class YouTubeInlineContainer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, CustomContainerInline obj) { var youtubeId = obj.FirstChild is Markdig.Syntax.Inlines.LiteralInline literalInline ? literalInline.Content.AsSpan().RightPart(' ').ToString() : null; if (string.IsNullOrEmpty(youtubeId)) { renderer.WriteLine($"Missing YouTube Id, Usage ::YouTube ::"); return; } renderer.WriteLine(@$" "); } } ``` That can be registered in `Configure.Ssg.cs` with: ```csharp MarkdigConfig.Set(new MarkdigConfig { ConfigureContainers = config => { // Add Custom Block or Inline containers config.AddInlineContainer("YouTube", new YouTubeInlineContainer()); } }); ``` Where it can then be used in your Markdown documentation: #### Input ```markdown ::YouTube MRQMBrXi5Sc:: ``` #### Output ::YouTube MRQMBrXi5Sc:: # Using Vue in Markdown Source: https://razor-press.web-templates.io/vue-in-markdown ## Progressive Enhancement Thanks to the Vue's elegant approach for progressively enhancing HTML content, Razor Press markdown documents can embed interactive reactive Vue components directly in Markdown which makes it possible to document the [Vue Tailwind Component Library](/vue/autoquerygrid) and its interactive component examples, embedded directly in Markdown. ## Markdown Documents are Vue Apps We can embed Vue components directly in Markdown simply because all Markdown Documents are themselves Vue Apps, which by default are created with the same [/mjs/app.mjs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/wwwroot/mjs/app.mjs) configuration that all Razor Pages uses. This allows using any [Vue DOM syntax](https://vuejs.org/guide/essentials/template-syntax.html) or global Vue components directly in Markdown, in the same way that they're used in Vue Apps defined in Razor or HTML pages. For example we can display the [GettingStarted.mjs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs) component that's on the home page with: #### Input ```html``` #### Output ## Markdown with Custom Vue Apps Pages that need advanced functionality beyond what's registered in the global App configuration can add additional functionality by adding a JavaScript module with the same **path** and **filename** of the markdown page with an `.mjs` extension: ``` /wwwroot/pages/ / .mjs ``` This is utilized by most [/pages/vue](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/wwwroot/pages/vue) Markdown pages to handle the unique requirements of each page's live examples. E.g. the [autoquerygrid.mjs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/wwwroot/pages/vue/autoquerygrid.mjs) uses a custom Vue App component that registers a custom **client** dependency and **Responsive** and **CustomBooking** components that are only used in [/vue/autoquerygrid.md](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/_pages/vue/autoquerygrid.md) page to render the interactive live examples in the [/vue/autoquerygrid](/vue/autoquerygrid) page: ```js import { onMounted } from "vue" import { Authenticate } from "./dtos.mjs" import { useAuth, useClient } from '@servicestack/vue' import { JsonApiClient } from "@servicestack/client" import Responsive from "./autoquerygrid/Responsive.mjs" import CustomBooking from "./autoquerygrid/CustomBooking.mjs" export default { install(app) { app.provide('client', JsonApiClient.create('https://blazor-gallery-api.jamstacks.net')) }, components: { Responsive, CustomBooking, }, setup() { const client = useClient() onMounted(async () => { const api = await client.api(new Authenticate({ provider: 'credentials', userName:'admin@email.com', password:'p@55wOrd' })) if (api.succeeded) { const { signIn } = useAuth() signIn(api.response) } }) return { } } } ``` ### Convert from Vue script setup This is roughly equivalent to the sample below if coming from VitePress or other npm Vue project that uses Vue's compile-time syntactic [` ``` ## Custom Styles in Markdown If needed custom styles can also be added for individual pages by adding a `.css` file at the following location: ``` /wwwroot/pages/ / .css ``` # Sidebars Source: https://razor-press.web-templates.io/sidebars ## Main Sidebar The sidebar defines the main navigation for your documentation, you can configure the sidebar menu in `_pages/sidebar.json` which adopts the same structure as [VitePress Sidebars](https://vitepress.dev/reference/default-theme-sidebar#sidebar), e.g: ```json [ { "text": "Introduction", "link": "/", "children": [ { "text": "What is Razor Press?", "link": "/what-is-razor-press" }, { "text": "Structure", "link": "/structure" } ] }, { "text": "Markdown", "children": [ { "text": "Sidebars", "link": "/sidebars" } ] } ] ``` ## Navigation Headings Primary navigation headings can optionally have `"links"` to make them linkable and `"icon"` to render them with a custom icon, e.g: ```json { "icon": "", "text": "Markdown", "link": "/markdown/", "children": [ ] } ``` ## Documentation Group Sidebars If your happy to use the same document page title for its menu item label, you can use an implicitly generated Sidebar navigation like [/creatorkit/](/creatorkit/about) uses for its Sidebar navigation which can be ordered with the **order** index defined in its frontmatter, e.g: ```yaml title: About order: 1 ``` Which can also be grouped into different navigation sections using the **group** frontmatter, e.g: ```yaml title: Overview order: 6 group: Portal ``` ### Custom Sidebars For more flexibility a custom sidebar can be defined for each group by defining a `sidebar.json` in its folder `_pages/ /sidebar.json` which [/vue/](/vue/install) uses for its explicit Sidebar Navigation, e.g: ```json [ { "text": "Vue", "link": "/vue", "children": [ { "text": "Install", "link": "/vue/install" } ] }, { "text": "Component Gallery", "children": [ { "text": "AutoQueryGrid", "link": "/vue/autoquerygrid" } ] } ] ``` # Redirects Source: https://razor-press.web-templates.io/redirects ## Static HTML Page Redirects The [redirects.json](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/redirects.json) file allows you to define a map of routes with what routes they should redirect to, e.g: ```json { "/creatorkit": "/creatorkit/", "/vue": "/vue/" } ``` When prerendering this will generate a `*.html` page for each mapping containing a [meta refresh](https://www.w3.org/TR/WCAG20-TECHS/H76.html) to perform a client-side redirect to the new route which will work in all static file hosts and CDNs like GitHub Pages CDN. Where it will redirect invalid routes like [/vue](https://razor-press.web-templates.io/vue) and [/creatorkit](https://razor-press.web-templates.io/creatorkit) to their appropriate `/vue/` and `/creatorkit/` paths. ## Redirects in AWS S3 Alternatively if you deploy your static site to a smarter static file host like an AWS S3 bucket you can perform these redirects on the server by defining them in [Custom Redirection Rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html). # Typesense Real-Time Search Source: https://razor-press.web-templates.io/typesense Many popular Open Source products use [Algolia DocSearch](https://docsearch.algolia.com) to power their real-time search features, however, it's a less appealing product for commercial products which is a paid service with a per request pricing model that made it difficult to determine what costs would be in the long run. We discovered [Typesense](https://typesense.org) as an appealing alternative which offers [simple cost-effective cloud hosting](https://cloud.typesense.org) but even better, they also have an easy to use open source option for self-hosting or evaluation. Given its effortless integration, simplicity-focus and end-user UX, it quickly became our preferred way to navigate [docs.servicestack.net](https://docs.servicestack.net). To make it easier to adopt Typesense's amazing OSS Search product we've documented the approach we use to create and deploy an index of our site automatically using GitHub Actions that you could also utilize in your Razor Press websites. Documentation search is a common use case which Typesense caters for with their [typesense-docsearch-scraper](https://github.com/typesense/typesense-docsearch-scraper) - a utility designed to easily scrape a website and post the results to a Typesense server to create a fast searchable index. ## Self hosting option We recommend using running their [easy to use Docker image](https://hub.docker.com/r/typesense/typesense/) to run an instance of their Typesense server, which you can run in a **t2.small** AWS EC2 instance or in a [Hetzner Cloud](https://www.hetzner.com/cloud) VM for a more cost effective option. Trying it locally, we used the following commands to spin up a local Typesense server ready to scrape out docs site. ```sh mkdir /tmp/typesense-data docker run -p 8108:8108 -v/tmp/data:/data typesense/typesense:0.21.0 \ --data-dir /data --api-key= --enable-cors ``` To check that the server is running, we can open a browser at `/health` and we get back 200 OK with `ok: true`. The Typesense server has a [REST API](https://typesense.org/docs/0.21.0/api) which can be used to manage the indexes you create. Or if you use their cloud offering, you can use their web dashboard to monitor and manage your index data. ## Populating the index With your local server is running, you can scrape your docs site using the [typesense-docsearch-scraper](https://github.com/typesense/typesense-docsearch-scraper). This needs some configuration to tell the scraper: - Where the Typesense server is - How to authenticate with the Typesense server - Where the docs website is - Rules for the scraper to follow extracting information from the docs website These [pieces of configuration come from 2 sources](https://github.com/ServiceStack/docs/tree/master/search-server/typesense-scraper). A [.env](https://github.com/ServiceStack/docs/blob/master/search-server/typesense-scraper/typesense-scraper.env) file related to the Typesense server information and a [.json](https://github.com/ServiceStack/docs/blob/master/search-server/typesense-scraper/typesense-scraper-config.json) file related to what site will be getting scraped. With a Typesense running locally on port **8108**, we configure the `.env` file with the following information: ``` TYPESENSE_API_KEY=${TYPESENSE_API_KEY} TYPESENSE_HOST=localhost TYPESENSE_PORT=8108 TYPESENSE_PROTOCOL=http ``` Next, we have to configure the `.json` config for the scraper. The **typesense-docsearch-scraper** has [an example of this](https://github.com/typesense/typesense-docsearch-scraper/blob/master/configs/public/typesense_docs.json) config in their repository. The default selectors will need to match the your websites HTML, which for Razor Press sites can start with the configuration, updated with your website domains: ```json { "index_name": "typesense_docs", "allowed_domains": ["docs.servicestack.net"], "start_urls": [ { "url": "https://docs.servicestack.net/" } ], "selectors": { "default": { "lvl0": "h1", "lvl1": ".content h2", "lvl2": ".content h3", "lvl3": ".content h4", "lvl4": ".content h5", "text": ".content p, .content ul li, .content table tbody tr" } }, "scrape_start_urls": false, "strip_chars": " .,;:#" } ``` With both the configuration files ready to use, we can run the scraper itself. The scraper is also available using the docker image `typesense/docsearch-scraper` which we can pass our configuration to, using the following command: ```sh docker run -it --env-file typesense-scraper.env \ -e "CONFIG=$(cat typesense-scraper-config.json | jq -r tostring)" \ typesense/docsearch-scraper ``` Here `-i` is used to reference a local `--env-file` and use `cat` and `jq` used to populate the `CONFIG` environment variable with the `.json` config file. ## Docker networking We had a slight issue here since the scraper itself is running in Docker via WSL and `localhost` doesn't resolve to our host machine to find the Typesense server also running in Docker. Instead we need to point the scraper to the Typesense server using the Docker local IP address space of `172.17.0.0/16` for it to resolve without additional configuration. We can see in the output of the Typesense server that it is running using `172.17.0.2`. We can swap the `localhost` with this IP address after which we see the communication between the servers flowing: ``` DEBUG:typesense.api_call:Making post /collections/typesense_docs_1635392168/documents/import DEBUG:typesense.api_call:Try 1 to node 172.17.0.2:8108 -- healthy? True DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 172.17.0.2:8108 DEBUG:urllib3.connectionpool:http://172.17.0.2:8108 "POST /collections/typesense_docs_1635392168/documents/import HTTP/1.1" 200 None DEBUG:typesense.api_call:172.17.0.2:8108 is healthy. Status code: 200 > DocSearch: https://docs.servicestack.net/azure 22 records) DEBUG:typesense.api_call:Making post /collections/typesense_docs_1635392168/documents/import DEBUG:typesense.api_call:Try 1 to node 172.17.0.2:8108 -- healthy? True DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 172.17.0.2:8108 DEBUG:urllib3.connectionpool:http://172.17.0.2:8108 "POST /collections/typesense_docs_1635392168/documents/import HTTP/1.1" 200 None ``` The scraper crawls the docs site following all the links in the same domain to get a full picture of all the content of our docs site. This takes a minute or so, and in the end we can see in the Typesense sever output that we now have **committed_index: 443**. ``` _index: 443, applying_index: 0, pending_index: 0, disk_index: 443, pending_queue_size: 0, local_sequence: 44671 I20211028 03:39:40.402626 328 raft_server.h:58] Peer refresh succeeded! ``` ## Searching content After you have a Typesense server with an index full of content, you'll want to be able to use it to search your docs site. You can query the index using `curl` which needs to known 3 key pieces of information: - Collection name, eg `typesense_docs` - Query term, `?q=test` - What to query, `&query_by=content` ```sh curl -H 'x-typesense-api-key: ' \ 'http://localhost:8108/collections/typesense_docs/documents/search?q=test&query_by=content' ``` The collection name and `query_by` come from how the scraper was configured. The scraper was posting data to the `typesense_docs` collection and populating various fields, eg `content`. Which as it returns JSON can be easily queried in JavaScript using **fetch**: ```js fetch('http://localhost:8108/collections/typesense_docs/documents/search?q=' + encodeURIComponent(query) + '&query_by=content', { headers: { // Search only API key for Typesense. 'x-typesense-api-key': 'TYPESENSE_SEARCH_ONLY_API_KEY' } }) ``` In the above we have also used a different name for the API key token, this is important since the `--api-key` specified to the running Typesense server is the admin API key. You don't want to expose this to a browser client since they will have the ability to create,update and delete your collections or documents. Instead we want to generate a "Search only" API key that is safe to share on a browser client. This can be done using the Admin API key and the following REST API call to the Typesense server. ```bash curl 'http://localhost:8108/keys' -X POST \ -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \ -H 'Content-Type: application/json' \ -d '{"description": "Search only","actions": ["documents:search"],"collections":["*"]}' ``` Now we can share this generated key safely to be used with any of our browser clients. ## Keeping the index updated A problem that becomes apparent when running the scraper is that it increases the size of the index since it currently doesn't detect and update existing documents. It wasn't clear if this is possible to configure from the current scraper, but we needed a way to achieve the following goals: - Update the search index automatically soon after docs have been changed - Don't let the index grow too big to avoid manual intervention - Have high uptime so documentation search is always available Typesense server itself performs extremely well, so a full update from the scraper doesn't generate an amount of load. However, every additional scrape uses additional disk space and memory that will eventually require periodically resetting and repopulating the index. One option is to switch to a new collection everytime the documentation is updated and delete the old collection, adopting a workflow that looks something like: 1. Docs are updated 2. Publish updated docs 3. Create new collection, store new and old names 4. Scrape updated docs 5. Update client with new collection 6. Delete old collection However this would require orchestration across a number of GitHub Action workflows which we anticipated would be fragile and non-deterministic as to how long it will take to scrape, update, and deploy our changes. ## Read-only Docker container The approach we ended up adopting was to develop and deploy read only Typesense Docker images containing an immutable copy of the index data in it as part of the GitHub Action deployments. In the case of Typesense, when it starts up, it reads from its `data` directory from disk to populate the index in memory and since our index is small and only updates when our documentation is updated, we can simplify the management of the index data by **baking it into the docker image**. This has several key advantages. - Disaster recovery doesn't need any additional data management. - Shipping an updated index is a normal ECS deployment. - Zero down time deployments. - Index is of a fixed size once deployed. ## Typesense Performance Search on our documentation site is a very light workload for Typesense. Running as an ECS service on a 2 vCPU instance, the service struggled to get close to **1%** whilst serving constant typeahead searching.  Since our docs site index is small (500 pages), the memory footprint is also tiny and stable at **~50MB** or **~10%** of the the service's soft memory limit.  This means we will be able to host this using a single EC2 instance among various other or the ServiceStack hosted example applications and use the same deployment patterns we've shared in our [GitHub Actions templates](https://docs.servicestack.net/mix-github-actions-aws-ecs). [](https://docs.servicestack.net/mix-github-actions-aws-ecs) Whilst this approach of shipping an index along with the Docker image isn't practical for large or 'living' indexes, many small to medium-sized documentation sites would likely benefit from the simplified approach of deploying readonly Docker images. ## GitHub Actions Workflow To create our own Docker image for our search server we need to perform the following tasks in our GitHub Action: 1. Run a local Typesense server in the GitHub Action using Docker 2. Scrape our hosted docs populating the local Typesense server 3. Copy the `data` folder of our local Typesense server during `docker build` Which is done with: ```sh mkdir -p ${GITHUB_WORKSPACE}/typesense-data cp ./search-server/typesense-server/Dockerfile ${GITHUB_WORKSPACE}/typesense-data/Dockerfile cp ./search-server/typesense-scraper/typesense-scraper-config.json typesense-scraper-config.json envsubst < "./search-server/typesense-scraper/typesense-scraper.env" > "typesense-scraper-updated.env" docker run -d -p 8108:8108 -v ${GITHUB_WORKSPACE}/typesense-data/data:/data \ typesense/typesense:0.21.0 --data-dir /data --api-key=${TYPESENSE_API_KEY} --enable-cors & # wait for typesense initialization sleep 5 docker run -i --env-file typesense-scraper-updated.env \ -e "CONFIG=$(cat typesense-scraper-config.json | jq -r tostring)" typesense/docsearch-scraper ``` Our `Dockerfile` then takes this data from the `data` folder during build. ```Dockerfile FROM typesense/typesense:0.21.0 COPY ./data /data ``` To avoid updating our search client between updates we also want to use the same **search-only API Key** everytime a new server is created. This can be achieved by specifying `value` in the `POST` command sent to the local Typesense server: ```sh curl 'http://172.17.0.2:8108/keys' -X POST \ -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \ -H 'Content-Type: application/json' \ -d '{"value": ,"description":"Search only","actions":["documents:search"],"collections":["*"]}' ``` If you're interested in adopting a similar approach you can find the whole GitHub Action workflow in our [search-index-update.yml](https://github.com/ServiceStack/docs/blob/master/.github/workflows/search-index-update.yml) workflow. ## Search UI Dialog After docs are indexed the only thing left to do is display the results. We set out to create a comparable UX to Algolia's doc search dialog which we've implemented in the [Typesense.mjs](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/wwwroot/mjs/components/Typesense.mjs) Vue component which you can register as a global component in your [app.mjs](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/wwwroot/mjs/app.mjs): ```js import Typesense from "./components/Typesense.mjs" const Components = { //... Typesense, } ``` Which renders as a **Search Button** that we've added next to our **Dark Mode Toggle** button in our [Header.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/Shared/Header.cshtml): ```html ```  The button also encapsulates the dialog component which uses Typesense REST API to query to our typesense instance: ```js fetch('https://search.docs.servicestack.net/collections/typesense_docs/documents/search?q=' + encodeURIComponent(query.value) + '&query_by=content,hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3&group_by=hierarchy.lvl0', { headers: { // Search only API key for Typesense. 'x-typesense-api-key': 'TYPESENSE_SEARCH_ONLY_API_KEY' } }) ``` This instructs Typesense to search through each documents content and h1-3 headings, grouping results by its page title. Refer to the [Typesense API Search Reference](https://typesense.org/docs/0.21.0/api/documents.html#search) to learn how to further fine-tune search results for your use-case. ## Search Results  The results are **excellent**, [see for yourself](https://docs.servicestack.net) by using the search at the top right or using `Ctrl+K` shortcut key on [docs.servicestack.net](https://docs.servicestack.net). It also does a great job handling typos and has quickly become the fastest way to navigate our extensive documentation that we hope also serves useful for implementing Typesense real-time search in your own documentation websites. # Installation Source: https://razor-press.web-templates.io/vue/install ## Manual Installation **@servicestack/vue** can be added to existing Vue SPA Apps by installing via npm: ```bash $ npm install @servicestack/vue ``` Where it will also install its **vue** and **@servicestack/client** dependencies. ## Installation-less option Alternatively you can take advantage of modern browsers [JS Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) support to use these libraries without installation by registering an [importmap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to define where it should load the ESM builds of these libraries from, e.g: ```html ``` For intranet Web Apps that need to work without internet access, save and reference local copies of these libraries, e.g: ```html ``` ## @Html.ImportMap Razor Pages or MVC Apps can use the `Html.ImportMaps()` to use local debug builds during development and optimal CDN hosted minified production builds in production: ```csharp @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") }) ``` > It's recommended to use exact versions to eliminate redirect latencies and to match the local version your App was developed against ### Polyfill for Safari Unfortunately Safari is the last modern browser to [support import maps](https://caniuse.com/import-maps) which is only now in Technical Preview. Luckily this feature can be polyfilled with the pre-configured [ES Module Shims](https://github.com/guybedford/es-module-shims): ```html @if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome"))) { } ``` ## Registration Then register the `@servicestack/vue` component library with your Vue app with: ```js import { JsonApiClient } from "@servicestack/client" import ServiceStackVue from "@servicestack/vue" const client = JsonApiClient.create() const app = createApp(component, props) app.provide('client', client) app.use(ServiceStackVue) //... app.mount('#app') ``` The **client** instance is used by API-enabled components to call your APIs using the [/api predefined route](/routing#json-api-pre-defined-route). ServiceStack Apps not running on .NET 6+ or have the **/api** route disabled should use `JsonServiceClient` instead: ```js const client = new JsonServiceClient() ``` ## Not using Vue Router Non SPA Vue Apps that don't use [Vue Router](https://router.vuejs.org) should register a replacement `` component that uses the browser's native navigation in [navigational components](/vue/navigation): ```js app.component('RouterLink', ServiceStackVue.component('RouterLink')) ``` # AutoQueryGrid Component Source: https://razor-press.web-templates.io/vue/autoquerygrid ## Default CRUD By default you can create an AutoQueryGrid that allows authorized users the ability to Create, Read, Update & Delete records with just the DataModel, e.g: ```html ``` This will utilize your App's existing [AutoQuery APIs](/autoquery/rdbms) for the specified DataModel to enable its CRUD functionality.## Read Only You can use `apis` to limit which AutoQuery APIs AutoQueryGrid should use, so if only the AutoQuery DTO is provided, the AutoQueryGrid will only be browsable in **read-only** mode: ```html ``` Table Styles
The same [DataGrid Table Styles](/vue/datagrid#table-styles) can also be used to style AutoQueryGrid, e.g: ```html``` **Custom Styles** The AutoQueryGrid's appearance is further customizable with the property classes & functions below: ```ts 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
Different AutoQueryGrid features can be hidden with `hide` and functionality disabled with `deny`, e.g: ```html``` Features that can be hidden and disabled include: ```ts defineProps<{ deny: "filtering" | "queryString" | "queryFilters" hide: "toolbar" | "preferences" | "pagingNav" | "pagingInfo" | "downloadCsv" | "refresh" | "copyApiUrl" | "filtersView" | "newItem" | "resetPreferences" }>() ```Global AutoQueryGrid Configuration
These features can also be disabled at a global level, applying to all `` components with [setConfig](/vue/use-config), e.g: ```js const { setAutoQueryGridDefaults } = useConfig() setAutoQueryGridDefaults({ hide: ['pagingNav','copyApiUrl','downloadCsv'] }) ``` Limit Columns
By default AutoQueryGrid displays all public properties returned in its AutoQuery API which can be further limited with `selected-columns`: ```html``` Simple Responsive Columns
Using `visible-from` is a simple way to enable a responsive DataGrid by specifying at which [Tailwind breakpoints](https://tailwindcss.com/docs/responsive-design) columns should be visible from and `header-titles` to use friendlier aliases for different columns, e.g: ```html``` Custom Responsive Columns
Which columns are displayed and how they're formatted are further customizable with `` slots: ```htmlRoom No Start Date End Date Employee ```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: ```html``` Data Reference Labels
[AutoQuery](/autoquery/rdbms) is able to infer relationships from the [POCO References](/ormlite/reference-support) of your Data Models where if your DataModel includes `[Reference]` attributes so that its related Data is returned in your AutoQuery APIs, AutoQueryGrid will be able to make use of it to render the Contacts & Job Names and Icons instead of just the plain Foreign Key Ids. An example of this in the [JobApplications](https://blazor-gallery.servicestack.net/locode/QueryJobApplications) DataModel DTO: ```csharp [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 ListComments { get; set; } public DateTime AppliedDate { get; set; } public JobApplicationStatus ApplicationStatus { get; set; } //... } ``` Which AutoQueryGrid uses to automatically display the Job and Contact name instead of their ids: ```html ``` With the original ids are discoverable by hovering over the Job & Contact labels. ## Reference Fields 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: ```csharp 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)]`. ## AutoQueryGrid Template Slots AutoQueryGrid supports a number of [Vue slots](https://vuejs.org/guide/components/slots.html) to customize its built-in UIs, including `formheader` and `formfooter` slots to insert custom content before and after the Auto Create & Edit components forms: ```html ``` This feature is used to implement [Locode's Audit History UI](/locode/auditing) for displaying the Audit History of each record in the bottom of the Edit Form for Authorized Users, implemented with: ```html``` Which loads the [AuditEvents.mjs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack/modules/locode/components/AuditEvents.mjs) component at the bottom of **Edit** forms, allowing Admin Users to inspect the Audit History of each record: [](/locode/auditing) Alternatively you can replace the entire Create and Edit Forms used with the `createform` and `editforms` slots: ```html ``` Additional toolbar buttons can be added with the `toolbarbuttons` slot, e.g: ```html ``` Alternatively you can replace the entire toolbar with your own with: ```html ``` All other template slots are passed down to the embedded [DataGrid](/vue/datagrid) component where they can be used to customize column headers and cells. ## AutoQueryGrid Properties Additional customizations available using AutoQueryGrid properties include: ```ts defineProps<{ filterDefinitions?: AutoQueryConvention[] id?: string apis?: string|string[] type?: string|InstanceType|Function prefs?: ApiPrefs deny?: string|GridAllowOptions|GridAllowOptions[] hide?: string|GridShowOptions|GridShowOptions[] selectedColumns?:string[]|string toolbarButtonClass?: string tableStyle?: TableStyleOptions gridClass?: string grid2Class?: string grid3Class?: string grid4Class?: string tableClass?: string theadClass?: string tbodyClass?: string theadRowClass?: string theadCellClass?: string headerTitle?:(name:string) => string headerTitles?: {[name:string]:string} visibleFrom?: {[name:string]:Breakpoint} rowClass?:(model:any,i:number) => string rowStyle?:(model:any,i:number) => StyleValue | undefined apiPrefs?: ApiPrefs canFilter?:(column:string) => boolean disableKeyBindings?:(column:string) => boolean configureField?: (field:InputProp) => void skip?: number create?: boolean edit?: string|number }>() ``` ## AutoQueryGrid Events Whilst the `headerSelected` and `rowSelected` events can be used to invoke custom functionality when column headers and rows are selected: ```ts defineEmits<{ (e: "headerSelected", name:string, ev:Event): void (e: "rowSelected", item:any, ev:Event): void }>() ``` ## Powers Locode AutoQueryGrid is already used extensively and is the key component that enables [Locode's](https://docs.servicestack.net/locode/) Instant Auto UI to manage your App's AutoQuery CRUD APIs. [](https://docs.servicestack.net/locode/) # DataGrid Component Source: https://razor-press.web-templates.io/vue/datagrid ## Default In its most simple usage the DataGrid component can be used to render typed collections: ```html ``` Which by default will display all object properties: Use **selected-columns** to control which columns to display and **header-titles** to use different column names: ```html``` Which for a wrist-friendly alternative also supports a string of comma delimited column names, e.g: ```html``` ## Simple Customizations Which columns are shown and how they're rendered is customizable with custom `` definitions: ```html Date {{ new Intl.DateTimeFormat().format(new Date(date)) }} {{ temperatureC }}° {{ temperatureF }}° {{ summary }} ```Column names can be changed with a **header-titles** alias mapping, or dynamically with a **header-title** mapping function. Alternatively for more advanced customizations, custom `` definitions can be used to control how column headers are rendered. If any custom column or header definitions are provided, only those columns will be displayed. Alternatively specify an explicit array of column names in **selected-columns** to control the number and order or columns displayed. ## Responsive A more advanced example showing how to implement a responsive datagrid defining what columns and Headers are visible at different screen sizes using **visible-from** to specify which columns to show from different Tailwind responsive breakpoints and `` definitions to collapse column names at small screen sizes: ```html Room No Start Date End Date Employee ```Behavior of the DataGrid can be customized with the `@header-selected` event to handle when column headers are selected to apply custom filtering to the **items** data source whilst the `@row-selected` event can be used to apply custom behavior when a row is selected. ## Using Formatters Your App and custom templates can also utilize @servicestack/vue's [built-in formatting functions](/vue/use-formatters) from: ```js import { useFormatters } from '@servicestack/vue' const { Formats, // Available format methods to use in formatValue, // Format any value or object graph currency, // Format number as Currency bytes, // Format number in human readable disk size link, // Format URL as link linkTel, // Format Phone Number as tel: link linkMailTo, // Format email as mailto: link icon, // Format Image URL as an Icon iconRounded, // Format Image URL as a full rounded Icon attachment, // Format File attachment URL as an Attachment hidden, // Format as empty string time, // Format duration in time format relativeTime, // Format Date as Relative Time from now relativeTimeFromMs, // Format time in ms as Relative Time from now formatDate, // Format as Date formatNumber, // Format as Number } = useFormatters() ``` Many of these formatting functions return rich HTML markup which will need to be rendered using Vue's **v-html** directive: ```html ``` The [PreviewFormat](/vue/formats) component also offers a variety of flexible formatting options. ## Table Styles The appearance of DataGrids can use **tableStyles** to change to different [Tailwind Table Styles](https://tailwindui.com/components/application-ui/lists/tables), e.g: ### Default (Striped Rows) ```html ``` ### Simple ```html``` ### Uppercase Headings ```html``` ### Vertical Lines ```html``` ### White Background ```html``` ### Full Width ```html``` ### Full Width, Uppercase with Vertical Lines ```html``` ## Using App Metadata By default DataGrid will render values using its default configured formatters, so results with strings, numbers and defaults will display a stock standard resultset: ```html``` Another option for formatting this dataset is to use the rich [format functions](/locode/formatters) in ServiceStack to annotate the DTOs with how each field should be formatted, e.g: ```csharp public class Booking { [AutoIncrement] public int Id { get; set; } public string Name { get; set; } public RoomType RoomType { get; set; } public int RoomNumber { get; set; } [IntlDateTime(DateStyle.Long)] public DateTime BookingStartDate { get; set; } [IntlRelativeTime] public DateTime? BookingEndDate { get; set; } [IntlNumber(Currency = NumberCurrency.USD)] public decimal Cost { get; set; } } ``` Which can be enabled when using [useMetadata](/vue/use-metadata) by specifying the `MetadataType` for the DataGrid's results in **type**: ```html``` Declaratively annotating your DTOs with preferred formatting hints makes this rich metadata information available to clients where it's used to enhance ServiceStack's built-in UI's and Components like: - [API Explorer](/api-explorer) - [Locode](https://docs.servicestack.net/locode/) - [Blazor Tailwind Components](/templates/blazor-components) # Auto Form Components Source: https://razor-press.web-templates.io/vue/autoform## AutoForm The `AutoForm` component is a generic form component 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: ```html ```Results
These Auto Form components are customizable with the [declarative C# UI Attributes](/locode/declarative#ui-metadata-attributes) where you can override the form's **heading** with `[Description]` and include a **subHeading** with `[Notes]` which supports rich HTML markup. **AutoForm Properties** Alternatively they can be specified in the components properties: ```ts defineProps<{ type: string|InstanceTypeResults
|Function modelValue?: ApiRequest|any heading?: string subHeading?: string showLoading?: boolean jsconfig?: string //= eccn,edv configureField?: (field:InputProp) => void /* Default Styles */ formClass?: string //= shadow sm:rounded-md innerFormClass?: string bodyClass?: string headerClass?: string //= p-6 buttonsClass?: string //= mt-4 px-4 py-3 bg-gray-50 dark:bg-gray-900 sm:px-6 flex justify-between headingClass?: string //= text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 subHeadingClass?: string submitLabel?: string //= Submit }>() ``` Both `@success` and `@error` events are fired after each API call, although built-in validation binding means it's typically unnecessary to manually handle error responses. ```ts defineEmits<{ (e:'success', response:any): void (e:'error', error:ResponseStatus): void (e:'update:modelValue', model:any): void }>() ``` **Model Binding** Forms can be bound to a Request DTO model where it can be used to pre-populate the Forms default values and Request DTO whereby specifying a **type** is no longer necessary: ```ts ``` ## Create Form `AutoCreateForm` can be used to create an automated form based on a [AutoQuery CRUD](/autoquery/crud) Create Request DTO definition which can be rendered in a traditional inline Form with **card** formStyle option, e.g: ```html ``` By default Auto Forms are rendered in a `SlideOver` dialog: ```html``` These Auto Forms are powered by the rich [App Metadata](/vue/use-metadata) surrounding your APIs, which contain all the necessary metadata to invoke the API and bind any contextual validation errors adjacent to the invalid field inputs. ## Edit Form `AutoEditForm` can be used to render an automated form based on Update and Delete [AutoQuery CRUD](/autoquery/crud) APIs which also makes use of **heading** and **sub-heading** customization options: ```html ``` The same form rendered in a traditional inline form with a **card** formStyle with some more advanced customization examples using rich markup in custom `` and `` slots: ```html ``` Change an existing Room Booking
Here are some
good tips on making room reservations The forms behavior and appearance is further customizable with the [API annotation](/locode/declarative#annotate-apis), declarative [validation](/locode/declarative#type-validation-attributes) and the custom [Field and Input](/locode/declarative#custom-fields-and-inputs) attributes, e.g: ```csharp [Description("Update an existing Booking")] [Notes("Find out how to create a C# Bookings App from Scratch")] [Route("/booking/{Id}", "PATCH")] [ValidateHasRole("Employee")] [AutoApply(Behavior.AuditModify)] public class UpdateBooking : IPatchDbChange an existing Room Booking
Here are some
good tips on making room reservations , IReturn { public int Id { get; set; } public string? Name { get; set; } public RoomType? RoomType { get; set; } [ValidateGreaterThan(0)] public int? RoomNumber { get; set; } [ValidateGreaterThan(0)] public decimal? Cost { get; set; } public DateTime? BookingStartDate { get; set; } public DateTime? BookingEndDate { get; set; } [Input(Type = "textarea")] public string? Notes { get; set; } public string? CouponId { get; set; } public bool? Cancelled { get; set; } } ``` Where they can be used to customize Auto Form's appearance from annotations on C# Server DTOs: ```html ``` ## Form Fields For more advanced customization of a Forms appearance and behavior, `AutoFormFields` can be used to just render the Form's fields (with validation binding) inside a custom Form which can submit the data-bound populated Request DTO to invoke the API, e.g: ```html ``` `toFormValues` is used when updating the data bound `request` DTO to convert API response values into the required format that HTML Inputs expect. # Form Inputs Components Source: https://razor-press.web-templates.io/vue/form-inputs## Bookings Form The `TextInput`, `SelectInput`, `CheckboxInput` and `TextAreaInput` contains the most popular Input controls used by C# POCOs which can be bound directly to Request DTOs and includes support for [declarative](/declarative-validation) and [Fluent Validation](/validation) binding. ```html ``` Which can be wired up to handle querying, updating and deleting including limiting functionality to authorized users with: ```html ``` This also shows how we can utilize `enumOptions` from our [App Metadata](/vue/use-metadata) to populate select drop downs from C# enums. # FileInput Component Source: https://razor-press.web-templates.io/vue/fileinput The ` ` component beautifies the browsers default HTML file Input, supporting both Single file: ```html ``` and Multiple File Uploads: ```html``` Use **files** when your binding to a `UploadedFile` complex type or **values** when binding to a `string[]` of file paths. When binding to relative paths, absolute URLs are resolved using [assetsPathResolver](/vue/use-config). ## 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: ```html ``` ## Integrates with Managed File Uploads Using [Managed File Uploads](/locode/files-overview) is a productive solution for easily managing file uploads where you can declaratively specify which location uploaded files should be written to, e.g: ```csharp public class UpdateContact : IPatchDb, IReturn { 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 ? 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](/vue/autoform) which supports invoking APIs with uploaded files: ```html ``` # TagInput Component Source: https://razor-press.web-templates.io/vue/taginput The `TagInput` component provides a user friendly control for managing a free-form `List` tags or symbols which is also supported in declarative Auto Forms using the `[Input(Type="tag")]` attribute as seen in the **UpdateContact** example using the [AutoForm components](/vue/autoform): ```html ``` Generated from the **UpdateContact** C# Request DTO: ```csharp public class UpdateContact : IPatchDb , IReturn { 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 ? 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; } } ``` Alternatively ` ` can be used in Custom Forms directly by binding to a `List ` or `string[]` model: ## Custom Form ```html ``` ## Allowable Values The list of allowable values can also be populated on C# Request DTO from a JavaScript expression: ```csharp public class MyRequest { [Input(Type = "tag", Options="{ allowableValues: ['c#','servicestack','vue'] }")] public List ? Skills { get; set; } } ``` Or from a [#Script Expression](https://sharpscript.net) in `EvalEvalAllowableValues` where it can be populated from a static list, e.g: ```csharp public class MyRequest { [Input(Type = "tag", EvalEvalAllowableValues="['c#','servicestack','vue']")] public List ? Skills { get; set; } } ``` Or sourced from a C# Expression, e.g: ```csharp public class MyRequest { [Input(Type = "tag", EvalEvalAllowableValues="AppData.Tags")] public List ? 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: ```csharp ScriptContext.Args[nameof(AppData)] = new AppData { Tags = db.Select ().Select(x => x.Name).ToList() }; ``` # Combobox Component Source: https://razor-press.web-templates.io/vue/combobox The `Combobox` component provides an Autocomplete Input optimized for searching a List of string values, Key Value Pairs or Object Dictionary, e.g: ```html ``` 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](/vue/autoform) on `string` or string collection properties with the `[Input(Type="combobox")]` [declarative UI Attribute](/locode/declarative#ui-metadata-attributes) on C# Request DTOs, e.g: ```csharp public class ComboBoxExamples : IReturn , 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 ? 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 ? 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 ? MultipleServerEntries { get; set; } } ``` Which can then be rendered with: ```html ``` **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](https://sharpscript.net) where they can be populated from a static list or from a C# class as seen in the examples referencing `AppData` properties: ```csharp public class AppData { public List AlphaValues { get; set; } public Dictionary AlphaDictionary { get; set; } public List > AlphaKeyValuePairs { get; set; } } ``` Which are populated on in the AppHost on Startup with: ```csharp 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 >` instead when you need to display an ordered list of Key/Value pairs. # Autocomplete Component Source: https://razor-press.web-templates.io/vue/autocomplete The `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. ```html ``` ## Custom Form # Modal Components Source: https://razor-press.web-templates.io/vue/modals## ModalDialog Use ` ` component to show any content inside a Modal Dialog: ```html Show Modal ``` Hello @servicestack/vue!
Show Modal Hello @servicestack/vue!
## SlideOver Use ` ` to show contents inside an animated slide over: ```html Show Slide a subtitle ```Authentication Required Sign In As seen in this example we can use **content-class** to customize the inner body contents and the `` slot to include an optional rich HTML subtitle, with all other inner contents is displayed in the SlideOver's body.Show Slide a subtitle Authentication Required Sign In ## SignIn The ` ` Component can be used to create an instant Sign Up form based on the [registered Auth Providers](/auth/) that handles Signing In authenticated users into Vue Apps with the [useAuth()](/vue/use-auth) APIs: ```html Hello, {{ user.displayName }}
```**SignIn Properties** ```ts defineProps<{ provider?: string // which Auth Provider to default to title?: string //= Sign In - Heading tabs?: boolean //= true - Show different Auth Provider tabs oauth?: boolean //= true - Show OAuth Provider buttons }>() ``` **Events** Use `@login` to run custom logic after successful authentication: ```ts defineEmits<{ (e:'login', auth:AuthenticateResponse): void }>() ``` # Navigation Components Source: https://razor-press.web-templates.io/vue/navigationHello, {{ user.displayName }}
## Tabs The ` ` 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. ```html ``` The Tab's Label can alternatively be overridden with a custom **label** function, e.g: ```html ``` **Tabs properties** ```ts defineProps<{ tabs: {[name:string]:Component } id?: string //= tabs param?: string //= tab - URL param to use label?: (tab:string) => string // - Custom function to resolve Tab Label selected?: string // - The selected tab tabClass?: string // - Additional classes for Tab Label bodyClass?: string // - Classes for Tab Body url?:boolean //= true - Whether to maintain active tab in history.pushState() }>() ``` ## Breadcrumbs Breadcrumb example: ```html ``` gallery Navigation Examples gallery Navigation Examples ## NavList Use `NavList` for rendering a vertical navigation list with Icons: ```html ``` DataGrid Component Examples for rendering tabular data Instant customizable UIs for calling AutoQuery CRUD APIs ## Link Buttons Using `href` with Button components will style hyper links to behave like buttons: ```html Vue.mjs Template Vue Component Docs ```Vue.mjs Template Vue Component Docs ## PrimaryButton That can use **color** to render it in different colors: ```html Default Blue Purple Red Green Sky Cyan Indigo ```Default Blue Purple Red Green Sky Cyan Indigo ## TextLink Tailwind `` hyper links, e.g: ```html docs.servicestack.net/vue ```That can also use **color** to render it in different colors: ```htmldocs.servicestack.net/vue Default Link Purple Link Red Link Green Link Sky Link Cyan Link Indigo Link ```# Alert Components Source: https://razor-press.web-templates.io/vue/alertsDefault Link Purple Link Red Link Green Link Sky Link Cyan Link Indigo Link ## Alert Show basic alert message:
```htmlDefault Message Information Message Success Message Warning Message Error Message ```Show alert message from dynamic HTML string: ```htmlDefault Message Information Message Success Message Warning Message Error Message ``` ## Alert Success Show success alert message: ```html Order was received ```Order was received ## Error Summary Show failed Summary API Error Message: ```html ``` # Format Examples Source: https://razor-press.web-templates.io/vue/formats## PreviewFormat Useful for rendering Table Cell data into different customizable formats, e.g: ### Currency ```html ``` ### Bytes ```html``` ### Icon ```html``` ### Icon Rounded ```html``` ### Icon with custom class ```html``` ### Attachment (Image) ```html``` ### Attachment (Document) ```html``` ### Attachment (Document) with classes ```html``` ### Link ```html``` ### Link with class ```html``` ### Link Email ```html``` ### Link Phone ```html``` ## Using Formatters Your App and custom templates can also utilize @servicestack/vue's [built-in formatting functions](/vue/use-formatters) from: ```js import { useFormatters } from '@servicestack/vue' const { Formats, // Available format methods to use informatValue, // Format any value or object graph currency, // Format number as Currency bytes, // Format number in human readable disk size link, // Format URL as link linkTel, // Format Phone Number as tel: link linkMailTo, // Format email as mailto: link icon, // Format Image URL as an Icon iconRounded, // Format Image URL as a full rounded Icon attachment, // Format File attachment URL as an Attachment hidden, // Format as empty string time, // Format duration in time format relativeTime, // Format Date as Relative Time from now relativeTimeFromMs, // Format time in ms as Relative Time from now formatDate, // Format as Date formatNumber, // Format as Number } = useFormatters() ``` Many of these formatting functions return rich HTML markup which will need to be rendered using Vue's **v-html** directive: ```html ``` ## HtmlFormat `HtmlFormat` can be used to render any Serializable object into a human-friendly HTML Format: ### Single Model ```html ``` ### Item Collections ```html``` ### Nested Complex Types ```html``` ### Nested Complex Types with custom classes When needed most default classes can be overridden with a custom **classes** function that can inspect the type, tag, depth, and row index to return a custom class. The TypeScript function shows an example of checking these different parameters to render a custom HTML resultset: ```html``` # App Metadata Source: https://razor-press.web-templates.io/vue/use-metadata The rich server metadata about your APIs that's used to generate your App's DTOs in [Multiple Programming Languages](/add-servicestack-reference), power ServiceStack's [built-in Auto UIs](/locode/declarative) also power the Metadata driven components in the **@servicestack/vue** component library where it can be loaded in your `_Layout.cshtml` using an optimal configuration like: ```html var dev = HostContext.AppHost.IsDevelopmentEnvironment(); @if (dev) { } ``` Where during development it always embeds the AppMetadata in each page but as this metadata can become quite large for systems with a lot of APIs, the above optimization clears and reloads the AppMetadata after **1 hr** or if the page was explicitly loaded with `?clear=metadata`, otherwise it will use a local copy cached in `localStorage` at `/metadata/app.json`, which Apps needing more fine-grained cache invalidation strategies can manage themselves. Once loaded the AppMetadata features can be access with the helper functions in [useMetadata](/vue/use-metadata). ```js import { useMetadata } from "@servicestack/vue" const { loadMetadata, // Load {AppMetadata} if needed setMetadata, // Explicitly set AppMetadata and save to localStorage clearMetadata, // Delete AppMetadata and remove from localStorage metadataApi, // Reactive accessor to ReftypeOf, // Resolve {MetadataType} for DTO name typeOfRef, // Resolve {MetadataType} by {MetadataTypeName} apiOf, // Resolve Request DTO {MetadataOperationType} by name property, // Resolve {MetadataPropertyType} by Type and Property name enumOptions, // Resolve Enum entries for Enum Type by name propertyOptions, // Resolve allowable entries for property by {MetadataPropertyType} createFormLayout, // Create Form Layout's {InputInfo[]} from {MetadataType} typeProperties, // Return all properties (inc. inherited) for {MetadataType} supportsProp, // Check if a supported HTML Input exists for {MetadataPropertyType} Crud, // Query metadata information about AutoQuery CRUD Types getPrimaryKey, // Resolve PrimaryKey {MetadataPropertyType} for {MetadataType} getId, // Resolve Primary Key value from {MetadataType} and row instance createDto, // Create a Request DTO instance for Request DTO name toFormValues, // Convert Request DTO values to supported HTML Input values formValues, // Convert HTML Input values to supported DTO values } = useMetadata() ``` For example you can use this to view all C# property names and Type info for the `Contact` C# DTO with: ```html ``` ## Enum Values and Property Options More usefully this can avoid code maintenance and duplication efforts from maintaining enum values on both server and client forms. An example of this is in the [Contacts.mjs](https://github.com/NetCoreTemplates/razor-tailwind/blob/main/MyApp/wwwroot/Pages/Contacts.mjs) component which uses the server metadata to populate the **Title** and **Favorite Genre** select options from the `Title` and `FilmGenre` enums: ```html ``` Whilst the `colorOptions` gets its values from the available options on the `CreateContact.Color` property: ```js const Edit = { //... setup(props) { const { property, propertyOptions, enumOptions } = useMetadata() const colorOptions = propertyOptions(property('CreateContact','Color')) return { enumOptions, colorOptions } //.. } } ``` Which instead of an enum, references the C# Dictionary in: ```csharp public class CreateContact : IPost, IReturn{ [Input(Type="select", EvalAllowableEntries = "AppData.Colors")] public string? Color { get; set; } //... } ``` To return a C# Dictionary of custom colors defined in: ```csharp public class ConfigureUi : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { //Enable referencing AppData.* in #Script expressions appHost.ScriptContext.Args[nameof(AppData)] = AppData.Instance; }); } public class AppData { public static readonly AppData Instance = new(); public Dictionary Colors { get; } = new() { ["#F0FDF4"] = "Green", ["#EFF6FF"] = "Blue", ["#FEF2F2"] = "Red", ["#ECFEFF"] = "Cyan", ["#FDF4FF"] = "Fuchsia", }; } ``` ## AutoForm Components See [Auto Form Components](/vue/autoform) docs for examples of easy to use, high productivity `AppMetadata` powered components. ## TypeScript Definition TypeScript definition of the API surface area and type information for correct usage of `useMetadata()` ```ts import type { AppMetadata, MetadataType, MetadataPropertyType, MetadataOperationType, InputInfo, KeyValuePair } from "./types" /** Load {AppMetadata} if needed * @param olderThan - Reload metadata if age exceeds ms * @param resolvePath - Override `/metadata/app.json` path use to fetch metadata * @param resolve - Use a custom fetch to resolve AppMetadata */ function loadMetadata(args: { olderThan?: number; resolvePath?: string; resolve?: () => Promise ; }): Promise ; /** Check if AppMetadata is valid */ function isValid(metadata: AppMetadata | null | undefined): boolean | undefined; /** Delete AppMetadata and remove from localStorage */ function setMetadata(metadata: AppMetadata | null | undefined): boolean; /** Delete AppMetadata and remove from localStorage */ function clearMetadata(): void; /** Query metadata information about AutoQuery CRUD Types */ const Crud: { Create: string; Update: string; Patch: string; Delete: string; AnyRead: string[]; AnyWrite: string[]; isQuery: (op: MetadataOperationType) => any; isCrud: (op: MetadataOperationType) => boolean | undefined; isCreate: (op: MetadataOperationType) => boolean | undefined; isUpdate: (op: MetadataOperationType) => boolean | undefined; isPatch: (op: MetadataOperationType) => boolean | undefined; isDelete: (op: MetadataOperationType) => boolean | undefined; model: (type?: MetadataType | null) => string | null | undefined; }; /** Resolve HTML Input type to use for {MetadataPropertyType} */ function propInputType(prop: MetadataPropertyType): string; /** Resolve HTML Input type to use for C# Type name */ function inputType(type: string): string; /** Check if C# Type name is numeric */ function isNumericType(type?: string | null): boolean; /** Check if C# Type is an Array or List */ function isArrayType(type: string): boolean; /** Check if a supported HTML Input exists for {MetadataPropertyType} */ function supportsProp(prop?: MetadataPropertyType): boolean; /** Create a Request DTO instance for Request DTO name */ function createDto(name: string, obj?: any): any; /** Convert Request DTO values to supported HTML Input values */ function toFormValues(dto: any, metaType?: MetadataType | null): any; /** Convert HTML Input values to supported DTO values */ function formValues(form: HTMLFormElement, props?: MetadataPropertyType[]): { [k: string]: any; }; /** * Resolve {MetadataType} for DTO name * @param name - Find MetadataType by name * @param [namespace] - Find MetadataType by name and namespace */ function typeOf(name?: string | null, namespace?: string | null): MetadataType | null; /** Resolve Request DTO {MetadataOperationType} by name */ function apiOf(name: string): MetadataOperationType | null; /** Resolve {MetadataType} by {MetadataTypeName} */ function typeOfRef(ref?: { name: string; namespace?: string; }): MetadataType | null; function property(typeName: string, name: string): MetadataPropertyType | null; /** Resolve Enum entries for Enum Type by name */ function enumOptions(name: string): { [name: string]: string; } | null; function enumOptionsByType(type?: MetadataType | null): { [name: string]: string; } | null; /** Resolve Enum entries for Enum Type by MetadataType */ function propertyOptions(prop: MetadataPropertyType): { [name: string]: string; } | null; /** Convert string dictionary to [{ key:string, value:string }] */ function asKvps(options?: { [k: string]: string; } | null): KeyValuePair [] | undefined; /** Create InputInfo from MetadataPropertyType and custom InputInfo */ function createInput(prop: MetadataPropertyType, input?: InputInfo): InputInfo; /** Create Form Layout's {InputInfo[]} from {MetadataType} */ function createFormLayout(metaType?: MetadataType | null): InputInfo[]; /** Return all properties (inc. inherited) for {MetadataType} */ function typeProperties(type?: MetadataType | null): MetadataPropertyType[]; /** Check if MetadataOperationType implements interface by name */ function hasInterface(op: MetadataOperationType, cls: string): boolean; /** Resolve PrimaryKey {MetadataPropertyType} for {MetadataType} */ function getPrimaryKey(type?: MetadataType | null): MetadataPropertyType | null; /** Resolve Primary Key value from {MetadataType} and row instance */ function getId(type: MetadataType, row: any): any; ``` # JSON API Client Features Source: https://razor-press.web-templates.io/vue/use-client [useClient()](https://github.com/ServiceStack/servicestack-vue/blob/main/src/api.ts) provides managed APIs around the `JsonServiceClient` instance registered in Vue App's with: ```js 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: ```js let { api, // Send a typed API request and return results in an ApiResult 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 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 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: ```js 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: ```js 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`: ```js let api = await client.api(new Hello(unRefs({ name }))) ``` ### setError `setError` can be used to populate client-side validation errors which the [SignUp.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/Pages/SignUp.mjs) component uses to report an invalid submissions when passwords don't match: ```js 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](/error-handling) 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](https://vue-mjs.web-templates.io/TodoMvc): ```html ``` 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 `` component checks for to display any field validation errors matching the field in `id` adjacent to the HTML Input: ```js 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](https://github.com/NetCoreTemplates/razor-tailwind/blob/main/MyApp/wwwroot/Pages/Contacts.mjs) `Edit` component for its [/Contacts](https://vue-mjs.web-templates.io/Contacts) page which doesn't do any manual error handling: ```js const Edit = { template:/*html*/` `, 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: Delete Update Contact ## 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](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/Pages/SignUp.mjs) for its [/signup](https://vue-mjs.web-templates.io/signup) page: ```js 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*/` `, 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](/vue/autoform) 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: ```html![]()
``` The AutoForm components are powered by your [App Metadata](/vue/use-metadata) which allows creating highly customized UIs from [declarative C# attributes](/locode/declarative) whose customizations are reused across all ServiceStack Auto UIs. ## TypeScript Definition TypeScript definition of the API surface area and type information for correct usage of `useClient()` ```ts /** Maintain loading state whilst API Request is in transit */ const loading: Ref/** Maintain API Error in reactive Ref */ const error: Ref /** 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 */ async function api (request:IReturn | 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 */ async function apiForm (request:IReturn | 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); ``` # Auth Features Source: https://razor-press.web-templates.io/vue/use-auth Vue.js Apps can access Authenticated Users using [useAuth()](/vue/use-auth) which can also be populated without the overhead of an Ajax request by embedding the response of the built-in [Authenticate API](https://vue-mjs.web-templates.io/ui/Authenticate?tab=details) inside `_Layout.cshtml` with: ```html ``` Where it enables access to the below [useAuth()](/vue/use-auth) utils for inspecting the current authenticated user: ```js const { signIn, // Sign In the currently Authenticated User signOut, // Sign Out currently Authenticated User user, // Access Authenticated User info in a reactive Ref isAuthenticated, // Check if the current user is Authenticated in a reactive Ref 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() ``` An example where this is used is in [Bookings.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/Pages/Bookings.mjs) to control whether the ` ` component should enable its delete functionality: ```js export default { template/*html*/:` `, setup(props) { const { hasRole } = useAuth() const canDelete = computed(() => hasRole('Manager')) return { canDelete } } } ``` ## TypeScript Definition TypeScript definition of the API surface area and type information for correct usage of `useAuth()` ```ts /** Access the currently Authenticated User info in a reactive Ref */ const user: Ref /** Check if the current user is Authenticated in a reactive Ref */ const isAuthenticated: Ref /** Sign In the currently Authenticated User */ function signIn(user:AuthenticateResponse): void; /** Sign Out currently Authenticated User */ function signOut(): void; /** Check if the Authenticated User has a specific role */ function hasRole(role:string): boolean; /** Check if the Authenticated User has a specific permission */ function hasPermission(permission:string): boolean; /** Check if the Authenticated User has the Admin role */ function isAdmin(): boolean; ``` # Formatting Functions and Methods Source: https://razor-press.web-templates.io/vue/use-formatters ### Using Formatters Your App and components can also utilize the built-in formatting functions in `useFormatters()`: ```js import { useFormatters } from '@servicestack/vue' const { Formats, // Available format methods to use in formatValue, // Format any value or object graph currency, // Format number as Currency bytes, // Format number in human readable disk size link, // Format URL as link linkTel, // Format Phone Number as tel: link linkMailTo, // Format email as mailto: link icon, // Format Image URL as an Icon iconRounded, // Format Image URL as a full rounded Icon attachment, // Format File attachment URL as an Attachment hidden, // Format as empty string time, // Format duration in time format relativeTime, // Format Date as Relative Time from now relativeTimeFromMs, // Format time in ms as Relative Time from now relativeTimeFromDate, // Format difference between dates as Relative Time formatDate, // Format as Date formatNumber, // Format as Number setDefaultFormats, // Set default locale, number and Date formats setFormatters, // Register additional formatters for use in indentJson, // Prettify an API JSON Response truncate, // Truncate text that exceeds maxLength with an ellipsis apiValueFmt, // Format an API Response value } = useFormatters() ``` Many of these formatting functions return rich HTML markup which will need to be rendered using Vue's **v-html** directive: ```html ``` See the [PreviewFormat](/vue/formats) for examples for what each of these format functions render to. ## Set global default formats Global default formats can be customized with `setDefaultFormats`: ```js setDefaultFormats({ locale: null, // Use Browsers default local assumeUtc: true, number: null, // Use locale Number format date: { method: "Intl.DateTimeFormat", options: "{dateStyle:'medium'}" }, maxFieldLength: 150, maxNestedFields: 150, maxNestedFieldLength: 150, }) ``` ## Register custom formatters Use `setFormatters` to register new formatters that you want to use in `[Format("method")]` or within ` ` components, e.g. you could register a formatter that renders a QR Code image of the content with: ```ts import { QRCode } from "qrcode-svg" function qrcode(content) { return new QRCode({ content, padding:4, width:256, height:256 }).svg() } setFormatters({ qrcode, }) ``` Where it will be able to be used within format components, e.g: ```html ``` That can also be used to decorate properties in C# DTOs with the [Format Attribute](/locode/formatters), e.g: ```csharp [Format("qrcode")] public string Code { get; set; } ``` ## Overriding built-in formatters `setFormatters` can also be to override the built-in formatting functions by registering alternative implementations for: ```ts setFormatters({ currency, bytes, link, linkTel, linkMailTo, icon, iconRounded, attachment, hidden, time, relativeTime, relativeTimeFromMs, formatDate, formatNumber, }) ``` ## TypeScript Definition TypeScript definition of the API surface area and type information for correct usage of `useFormatters()` ```ts class Formats { static currency: FormatInfo; static bytes: FormatInfo; static link: FormatInfo; static linkTel: FormatInfo; static linkMailTo: FormatInfo; static icon: FormatInfo; static iconRounded: FormatInfo; static attachment: FormatInfo; static time: FormatInfo; static relativeTime: FormatInfo; static relativeTimeFromMs: FormatInfo; static date: FormatInfo; static number: FormatInfo; static hidden: FormatInfo; } /** Format any value or object graph */ function formatValue(value: any, format?: FormatInfo | null, attrs?: any): any; /** Format number as Currency */ function currency(val: number, attrs?: any): string; /** Format number in human readable disk size */ function bytes(val: number, attrs?: any): string; /** Format URL as link */ function link(href: string, opt?: { cls?: string; target?: string; rel?: string; }): string; /** Format email as mailto: link */ function linkMailTo(email: string, opt?: { subject?: string; body?: string; cls?: string; target?: string; rel?: string; }): string; /** Format Phone Number as tel: link */ function linkTel(tel: string, opt?: { cls?: string; target?: string; rel?: string; }): string; /** Format Image URL as an Icon */ function icon(url: string, attrs?: any): string; /** Format Image URL as a full rounded Icon */ function iconRounded(url: string, attrs?: any): string; /** Format File attachment URL as an Attachment */ function attachment(url: string, attrs?: any): string; /** Format as empty string */ function hidden(o: any): string; /** Format duration in time format */ function time(o: any, attrs?: any): string; /** Format Date as Relative Time from now */ function relativeTime(val: string | Date | number, rtf?: Intl.RelativeTimeFormat): string | undefined; /** Format difference between dates as Relative Time */ function relativeTimeFromDate(d: Date, from?: Date): string | undefined; /** Format time in ms as Relative Time from now */ function relativeTimeFromMs(elapsedMs: number, rtf?: Intl.RelativeTimeFormat): string | undefined; /** Format as Date */ function formatDate(d: Date | string | number, attrs?: any): string; /** Format as Number */ function formatNumber(n: number, attrs?: any): string; /** Set default locale, number and Date formats */ function setDefaultFormats(newFormat: DefaultFormats): void; /** Register additional formatters for use in */ function setFormatters(formatters: { [name: string]: Function; }): void; /** Prettify an API JSON Response */ function indentJson(o: any): string; /** Truncate text that exceeds maxLength with an ellipsis */ function truncate(str: string, maxLength: number): string; /** Format an API Response value */ function apiValueFmt(o: any, format?: FormatInfo | null, attrs?: any): any; ``` # File Utils Source: https://razor-press.web-templates.io/vue/use-files The file utils are utilized by the ` ` [Input component](/vue/form-inputs) and `icon`, `iconRounded` and `attachment` [formatters](/vue/use-formatters) for resolving file SVG Icons and MIME Types that Apps can also utilize in `useFiles()` ```js import { useFiles } from '@servicestack/vue' const { extSvg, // Resolve SVG XML for file extension extSrc, // Resolve SVG Data URI for file extension getExt, // Resolve File extension from file name or path canPreview, // Check if path or URI is of a supported web image type getFileName, // Resolve file name from /file/path getMimeType, // Resolve the MIME type for a file path name or extension formatBytes, // Format file size in human readable bytes filePathUri, // Resolve the Icon URI to use for file encodeSvg, // Encode SVG XML for usage in Data URIs svgToDataUri, // Convert SVG XML to data:image URL fileImageUri, // Resolve image preview URL for file objectUrl, // Create and track an Object URL for an uploaded file flush, // Release all tracked Object URLs inputFiles, // Resolve file metadata for all uploaded HTML input files iconOnError, // Error handler for broken images to return a fallbackSrc iconFallbackSrc, // Resolve the fallback URL for a broken Image URL } = useFiles() ``` ## TypeScript Definition TypeScript definition of the API surface area and type information for correct usage of `useFiles()` ```ts /** Resolve SVG XML for file extension */ function extSvg(ext: string): string | null; /** Resolve SVG URI for file extension */ function extSrc(ext: string): any; /** Resolve File extension from file name or path */ function getExt(path?: string | null): string | null; /** Check if path or URI is of a supported web image type */ function canPreview(path: string): boolean; /** Resolve file name from /file/path */ function getFileName(path?: string | null): string | null; /** Resolve the MIME type for a file path name or extension */ function getMimeType(fileNameOrExt: string): string; /** Format file size in human readable bytes */ function formatBytes(bytes: number, d?: number): string; /** Resolve the Icon URI to use for file */ function filePathUri(path?: string): string | null; /** Encode SVG XML for usage in Data URIs */ function encodeSvg(s: string): string; /** Convert SVG XML to data:image URL */ function svgToDataUri(svg: string): string; /** Resolve image preview URL for file */ function fileImageUri(file: any | { name: string; }): string | null; /** Create and track Image URL for an uploaded file */ function objectUrl(file: Blob | MediaSource): string; /** Release all tracked Object URLs */ function flush(): void; /** Resolve file metadata for all uploaded HTML file input files */ function inputFiles(input: HTMLInputElement): { fileName: string; contentLength: number; filePath: string | null; }[] | null; /** Error handler for broken images to return a fallbackSrc */ function iconOnError(img: HTMLImageElement, fallbackSrc?: string): void; /** Resolve the fallback URL for a broken Image URL */ function iconFallbackSrc(src: string, fallbackSrc?: string): string | null; ``` # Vue Tailwind Global Configuration Source: https://razor-press.web-templates.io/vue/use-config ## Manage Global Configuration `useConfig` is used to maintain global configuration that's used throughout the Vue Component Library. ```ts import { useConfig } from "@servicestack/vue" const { config, // Resolve configuration in a reactive Ref setConfig, // Set global configuration assetsPathResolver, // Resolve Absolute URL to use for relative paths fallbackPathResolver, // Resolve fallback URL to use if primary URL fails autoQueryGridDefaults, // Resolve AutoQueryGrid default configuration setAutoQueryGridDefaults, // Set AutoQueryGrid default configuration } = useConfig() ``` The asset and fallback URL resolvers are useful when hosting assets on a separate CDN from the hosted website. ### Default configuration ```js setConfig({ redirectSignIn: '/signin', assetsPathResolver: src => src, fallbackPathResolver: src => src, }) ``` ## AutoQueryGrid Defaults Use `setAutoQueryGridDefaults` to change the default configuration for all [AutoQueryGrid](/vue/autoquerygrid) components: ```ts const { setAutoQueryGridDefaults } = useConfig() setAutoQueryGridDefaults({ deny: [], hide: [], toolbarButtonClass: undefined, tableStyle: "stripedRows", take: 25, maxFieldLength: 150, }) ``` TypeScript Definitions for available AutoQueryGridDefaults: ```ts type AutoQueryGridDefaults = { deny?:GridAllowOptions[] hide?:GridShowOptions[] toolbarButtonClass?: string tableStyle?: TableStyleOptions take?:number maxFieldLength?: number } export type GridAllowOptions = "filtering" | "queryString" | "queryFilters" export type GridShowOptions = "toolbar" | "preferences" | "pagingNav" | "pagingInfo" | "downloadCsv" | "refresh" | "copyApiUrl" | "resetPreferences" | "filtersView" | "newItem" ``` ## TypeScript Definition TypeScript definition of the API surface area and type information for correct usage of `useConfig()` ```ts interface UiConfig { redirectSignIn?: string assetsPathResolver?: (src:string) => string fallbackPathResolver?: (src:string) => string } /** Resolve configuration in a reactive Ref */ const config:ComputedRef /** Set global configuration */ function setConfig(config: UiConfig): void; /** Resolve Absolute URL to use for relative paths */ function assetsPathResolver(src?: string): string | undefined; /** Resolve fallback URL to use if primary URL fails */ function fallbackPathResolver(src?: string): string | undefined; ``` # General Utils Source: https://razor-press.web-templates.io/vue/use-utils General utils used by Vue Components you may also find useful in your Apps: ```js import { useUtils } from "@servicestack/vue" const { dateInputFormat, // Format Date into required input[type=date] format timeInputFormat, // Format TimeSpan or Date into required input[type=time] format setRef, // Double set reactive Ref to force triggering updates unRefs, // Returns a dto with all Refs unwrapped transition, // Update reactive `transition` class based on Tailwind animation transition rule-set focusNextElement, // Set focus to the next element inside a HTML Form getTypeName, // Resolve Request DTO name from a Request DTO instance htmlTag, // HTML Tag builder htmlAttrs, // Convert object dictionary into encoded HTML attributes linkAttrs, // Convert HTML Anchor attributes into encoded HTML attributes toAppUrl, // Resolve Absolute URL from relative path isPrimitive, // Check if value is a scalar type isComplexType, // Check if value is a non-scalar type } = useUtils() ``` ## TypeScript Definition TypeScript definition of the API surface area and type information for correct usage of `useUtils()` ```ts /** Format Date into required input[type=date] format */ declare function dateInputFormat(d: Date): string; /** Format TimeSpan or Date into required input[type=time] format */ declare function timeInputFormat(s?: string | number | Date | null): string; /** Double set reactive Ref to force triggering updates */ declare function setRef($ref: Ref , value: any): void; /** Returns a dto with all Refs unwrapped */ declare function unRefs(o: any): any; /** Update reactive `transition` class based on Tailwind animation transition rule-set */ declare function transition(rule: TransitionRules, transition: Ref , show: boolean): void; /** Set focus to the next element inside a HTML Form */ declare function focusNextElement(): void; /** Resolve Request DTO name from a Request DTO instance */ declare function getTypeName(dto: any): any; /** HTML Tag builder */ declare function htmlTag(tag: string, child?: string, attrs?: any): string; /** Convert object dictionary into encoded HTML attributes */ declare function htmlAttrs(attrs: any): string; /** Convert HTML Anchor attributes into encoded HTML attributes */ declare function linkAttrs(attrs: { href: string; cls?: string; target?: string; rel?: string; }): { target: string; rel: string; class: string; } & { href: string; cls?: string | undefined; target?: string | undefined; rel?: string | undefined; }; /** Resolve Absolute URL from relative path */ declare function toAppUrl(url: string): string | undefined; /** Check if value is a scalar type */ declare function isPrimitive(value: any): boolean; /** Check if value is a non-scalar type */ declare function isComplexType(value: any): boolean; ```