ServiceStack Markdown Editor is a developer-friendly Markdown Editor for Vuetify Apps which is optimized GitHub Flavored Markdown where it supports popular short-cuts for editing and documenting code like tab block un/indenting, single-line and code and comments blocks.
This component was built for techstacks.io website where it's used extensively for all posts, comments and anywhere else allowing rich markup using Markdown.
$ npm install @servicestack/editor
Import and register the Editor Vuetify component with your Vue Component to use it like a Vue Input Control, e.g:
<template> <Editor v-model="content" label="Markdown" /> </template> <script> import Editor from "@servicestack/editor"; export default { components: { Editor }, } </script>
As it's a wrapper around a Vuetify text field it has access to a lot of the rich standard functionality available in Vuetify controls:
Vuetify properties:
v-modellabelcounterrowsruleserrorMessagesautofocusdisabled
Editor properties:
lang- which language to use for syntax highlighting in code comment blocks
Events:
@save- method to invoke when user clicks Save icon orCtrl+Skeyboard shortcut@close- method to invoke when user presses theESCkey
For added productivity the Editor supports many of the popular Keyboard shortcuts in found in common IDEs:
Note: pressing the shortcut multiple times toggles on/off the respective functionality
The CommentEdit.vue shows a nice small and complete example of using the Editor for editing existing comments or submitting new ones.
It provides a user-friendly UX with declartive client and server-side validation where 'content' field validation errors
show up next to the Editor dialog whilst other errors are displayed in the FORM's <v-alert/> summary message.
Annotations were added to the implementation below to describe how the component works:
<template> <v-form v-model="valid" ref="form" lazy-validation> <v-card> <v-card-text> <v-alert outline color="error" icon="warning" :value="errorMessage()"> {{ errorMessage() }} </v-alert> <Editor ref="editor" label="Comment" v-model="content" :rows="6" :counter="1000" :rules="[ v => !v || v.length <= 1000 || 'Max 1000 characters' ]" :error-messages="errorResponse('content')" :lang="csharp" :autofocus="true" @save="submit" @close="reset()" /> </v-card-text> <v-card-actions> <v-layout> <v-btn flat @click="submit">Submit</v-btn> <v-btn v-if="replyId || comment" flat @click="reset(false)">Close</v-btn> </v-layout> </v-card-actions> </v-card> </v-form> </template> <script> import Editor from "@servicestack/editor"; import { mapGetters } from "vuex"; import { errorResponse } from "@servicestack/client"; import { createPostComment, updatePostComment } from "~/shared/gateway"; // Editable comment fields const comment = { postId: null, content: null }; export default { components: { Editor }, props: ['post', 'comment', 'replyId', 'autofocus'], methods: { // Show top-level error messages or errors for the 'postId' field in the <v-alert/> summary dialog errorMessage() { return this.errorResponse() || this.errorResponse('postId'); }, // Clear the form back to its original state reset(added){ this.responseStatus = this.content = null; this.valid = true; this.$emit('done', added); }, // Submit the comment to the server and overlay any error responses back on the form async submit() { if (this.$refs.form.validate()) { // Check if form passes all client validation rules try { this.$store.commit('loading', true); // indicate to the App that an API request is pending // If component was initialized with an existing `comment` update it, otherwise create anew const response = this.comment != null ? await updatePostComment(this.comment.id, this.post.id, this.content) : await createPostComment(this.postId, this.content, this.replyId); this.reset(true); // Clear the form back to a new state when successful } catch(e) { this.valid = false; // mark this form as invalid this.responseStatus = e.responseStatus || e; // populate the server error response } finally { this.$store.commit('loading', false); // indicate to the App that the API request is done } } }, // Register `errorResponse` function so it's available in the template errorResponse, }, // Initialize the component data from its properties mounted() { if (this.post) { this.postId = this.post.id; } if (this.comment) { this.content = this.comment.content; } }, data: () => ({ ...comment, // Create reactive properties for all `comment` fields valid: true, // Holds whether the form is in an invalid state requiring user input to correct responseStatus: null, // Captures the servers structured error response }), } </script>
The errorResponse method from the @servicestack/client npm package is opinionated
in handling ServiceStack's structured API Response Errors but will work for any API returning
the simple error response schema below:
{
"errorCode": "ErrorCode",
"message": "Descriptive Summary Error Message",
"errors": [
{
"fieldName": "content",
"errorCode": "NotEmpty",
"message": "Descriptive Error Message for 'content' Field"
}
]
}Which will check the components this.responseStatus property to return different error messages based on what field it was called with, e.g:
errorResponse() //= Descriptive Summary Error Message errorResponse('postId') //= undefined errorResponse('content') //= Descriptive Error Message for Field Error
ServiceStack Customers can ask questions, report issues or submit feature requests from the ServiceStack Community.
If you're not a Customer, please ask Questions on StackOverflow with the [servicestack] hash tag.
Pull Requests for fixes and new features are welcome!