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-model
label
counter
rows
rules
errorMessages
autofocus
disabled
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+S
keyboard shortcut@close
- method to invoke when user presses theESC
key
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!