Integrating Tiny with Vue in a real world application
Important: The information in this article is over 12 months old, and may be out of date or no longer relevant.
But hey, you're here anyway, so give it a read see if it still applies to you.
TinyMCE 5 has made the setup process more straightforward - and even easier with the Cloud version. And as there are so many JS frameworks out there, Tiny have also made a bundle of different integrations, and one is for Vue.
I really do love Vue - I find it solid to develop for, easy to implement, and can also be used as little or as much as you need, be that a single page app, a traditional multi-page site, or even just a small component on a site. I love its flexibility.
But in saying that, flexibility also comes with so many ways to solve the problem at hand. So before we go too far, this is just one way of looking at Tiny and Vue in a real-world type approach, and uses Webpack to bundle it up. Maybe you want to use VueCLI, maybe you’re a Laravel dev with Mix - my point is, I don’t want to force you to change your workflow to do something a specific way, but hopefully you find this article and its concepts useful so that the ideas can be applied to your structure, project and tools.
This setup has been used in a few recent projects of mine, and while there is a little bit of setup involved, it is easy to maintain, read and customise for each project.
Just a heads up as you're reading... some code samples will include "..." purely to keep the snippets shorter. If I'm introducing something new, it will be a complete sample, but if it is then a tweak to something we have done, the "..." replaces code that isn't shifting. Just letting you know that not every piece of code here can be copy-and-pasted and run.
Pre-requisites
This article is aimed at Vue developers - so experience with Vue 2, ES6 and a build tool such as Webpack 4 and running scripts to build your source code. Experience with setting up and configuring Tiny 5 is needed too, even if it is from plain vanilla JS or jQuery - we’ll look more at the Vue/Tiny side of things here.
Want a quickstart guide to getting started with Tiny in Vue? Check out Tiny's guide on how to set up their WYSIWYG HTML editor in Vue.
It is up to you to have your base up and running - how you do that is totally up to you - NodeJS, Apache, IIS - your tech, your choice. I’ve written this assuming you’re at least up and running.
What do we want to achieve?
At a high level, we want to be able to use Tiny from within our Vue app. For the purpose of this article, let’s say we’re wanting to do this using Vue Single File Components. Imagine it is for an online editor, like a CMS.
This means we need to be able to put content in to the Tiny instance, as well as read changes back.
Given we want to look at this from a bit of a real-world perspective, let’s also consider how we can make this easier to re-use, with centralised configuration within our app.
Oh, and because Tiny plugins are so easy (and valuably useful) to write, we will also consider how to set it all up to allow for including our plugins in our build process.
Where do we start?
Tiny have done a brilliant job of creating an integration for Vue and making it very straight forward (and insanely flexible).
However, in a real-world app, you may have the need to re-use Tiny in several places. And while the Tiny’s integration is straight forward, what we’ll look at here is how to create our own Tiny component that can tick off our list of requirements above, and also make it easier to re-use.
This also means that if we need to extend or change our Tiny setup, we can do so in one place that can then be picked up by the rest of our system.
Following on from Tiny’s article, you can see how easy it is to configure your Tiny instance in Vue. We are going to take this and wrap it in our own re-usable component.
Fun fact: Tiny recently posted about how to get the most out of the performance of using the cloud version of TinyMCE- and Tiny’s Vue component does all that hard work for you, so while it’s a good read, it’s also handy to know that Tiny have done this for us already.
Structure
To help modularise what we are doing, our app would typically need three things:
Our Tiny component
A helper for Tiny configuration (that gets tinkered during build)
A helper for app configuration (that may need to be tinkered after build)
Firstly, our Tiny component - basically we’ll be making a reusable Vue component that is able to pull in our Tiny configuration and create a two-way bound component for the content within the editor.
Secondly, the helper for Tiny configuration will move all of the configuration for our Tiny setup in to an external file - I like to do this for readability and separating the config (that can change from project-to-project) from the actual heavy lifting of the Vue component.
And finally, our helper for app configuration. This is where we will get our Tiny Cloud API key from. In the real world, this app-level config helper would also be able to store your other app config - maybe endpoint setup for your backend. While we’re using a simple object in this example, you could also approach this however fits for your project (i.e. could be through Vuex if your project is using it).
We will build these backwards - just so we can see where we get what from.
App-level config
For this example, we’ll have a JSON object in our served HTML code that includes our app-level config. Remember that this is just the easiest way to do this for this example - you can get our config however works for your project.
1<script id="app-config" type="application/json">2 {3 "tiny": {4 "key": "YOUR_TINY_API_KEY"5 }6 }7</script>
We’ve only got a single key here, but in more complex projects, this could include other app-wide config settings that may come from your backend.
Let’s create an appConfig module that we can use throughout our app.
1'use strict'; 2 3let appConfig = {}; 4 5// 6// If we have the "app-config" element, let's load its contents into our config object. 7// 8if (document.getElementById('app-config')) { 9 // load the config options10 appConfig = JSON.parse(document.getElementById('app-config').innerHTML);11}12 13/**14 * Get a config option, using dot-syntax.15 *16 * Will parse the requested key by a period to look for nested attributes.17 *18 * For example, "tiny.key" would look for the "key" parameter inside a "tiny" object.19 *20 * @param key21 * @return {*}22 */23appConfig.get = function (key) {24 return key.split('.').reduce((o, i) => o[i], this);25};26 27// freeze our object28Object.freeze(appConfig);29 30export default appConfig;
Basically we’re reading our JSON content, and placing this into an object. We’ve also defined a dot-syntax “get” method to allow us to get the “key” of our Tiny config by calling:
1appConfig.get('tiny.key')
When we want to use our App config, we can simply import the appConfig in to our current module. This also means that our Tiny API key is external to our built and compiled code - if, for whatever reason, the API key is changed, we don’t need to rebuild our project - we can update our app’s config, and then Vue will be able to pick it up from there.
We’ll do a similar thing for our Tiny config too.
Tiny config
Our Tiny config helper uses the same concept - a simple module that exports a helper object.
I like to separate the Tiny config to a helper like this for readability and code maintainability.
If you are using a simple instantiation of Tiny that has a single set of configuration options, this could be placed in your Tiny component too. For complex use cases, separating out to this level of modularity can make your life easier.
Our Tiny config will contain:
Our actual Tiny config, just as if we were calling “init” in vanilla JS
Configurations for different toolbar setups
Central point to get our API key
Helper functions to deliver our different Tiny configuration objects
So here’s one other tip too: the Vue integration from Tiny does allow many of the configuration options to be passed as props - however, if you’re coming from an outside-of-Vue Tiny background, you may be familiar with the object-based approach. After all, this is how Tiny documents configuration (and provides examples) so I think it makes sense to continue using this methodology, rather than converting to a framework-specific approach.
This should actually be great news too - simply create your object of configuration options as if you were integrating in vanilla JS. Yep, simple as that.
1let config = { 2 selector: 'textarea', 3 4 schema: "html5", 5 6 plugins: [ 7 "advlist autolink autosave autoresize link image lists charmap hr anchor", 8 "searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime nonbreaking", 9 "table template colorpicker paste textcolor importcss textpattern spellchecker"10 ],11 12 menubar: false13};
When using Tiny, there are many times I need to have different configurations based on how Tiny is being used in the app. Sometimes I need a fully-featured setup, but other times a really simple toolbar will suffice.
For this example, let’s say there are two configurations: “full” and “simple”, and they each have their own toolbar configurations:
1let editorType = { 2 full: { 3 toolbar: [ 4 'undo redo | bold italic underline strikethrough | alignleft aligncenter alignright | blockquote | formatselect | spellchecker', 5 'cut copy paste removeformat | searchreplace | bullist numlist | outdent indent | hr | link unlink anchor image code | inserttime', 6 'table | subscript superscript | charmap | visualchars visualblocks nonbreaking | template | helloworld' 7 ] 8 }, 9 simple: {10 toolbar: [11 'undo redo | bold italic underline strikethrough | alignleft aligncenter alignright | blockquote | table | spellchecker',12 'cut copy paste removeformat | searchreplace | bullist numlist | code | charmap | visualblocks | link unlink'13 ],14 }15};
Configuration could also extend to the menubar, or even other Tiny configuration options.
Basically each key in our editorType object is an additional object that we will apply over the top of our core Tiny config.
Let’s just recap that:
We’ve got our base Tiny config - this applies to all editor instances
We’ve got our editorType config that contains rules specific to a certain type of editor setup
By separating the two, we can then create a single object based on our core Tiny config, and merge in our editor-type config:
1let tinyConfig = { 2 getConfig: function (key = 'full') { 3 if (!key || !editorType.hasOwnProperty(key)) 4 { 5 // default to full if there is a key, but it doesn't exist 6 key = 'full'; 7 } 8 return Object.assign({}, config, editorType[key]); 9 }10};
As we add more editor types - what if we needed a “lite” editor type - then this simple getConfig helper that we’ve written will take our core Tiny config, and apply the type-specific config over the top.
Given how big a Tiny config object can become, this is fantastic at separating the replicated config (the core Tiny config) from the editor-specific config (editorType).
Make sense?
While we’re getting the config, I’ve also found it useful to centralise the Tiny API key call. We’ll just create a new function getApiKey that gets our Tiny API key from the appConfig module. This makes our tinyConfig helper be a simple object with two functions:
1let tinyConfig = { 2 getAPIKey: function () { 3 return appConfig.get('tiny.key'); 4 }, 5 getConfig: function (key = 'full') { 6 if (!key || !editorType.hasOwnProperty(key)) 7 { 8 // default to full if there is a key, but it doesn't exist 9 key = 'full';10 }11 return Object.assign({}, config, editorType[key]);12 }13};
Wait one sec…
Why have we created this simple getApiKey function when we could call appConfig directly?
Good question - and you can do that, definitely. But also this moves the call that I need for the Tiny API key to a single module - the Tiny config module - so when I need to instantiate Tiny, I can import the tinyConfig module and get everything I need - the tinyConfig module does the work of importing the appConfig module behind the scenes. Just my way of approaching it - you can, however, go whichever way suits you best.
Building our Tiny component
So far we just have two helper modules - the App-level config can be built and configured however your app needs to work, and the Tiny config is a great way to centralise and modularise all of your Tiny configuration, and is especially useful as your configurations grow.
In the real world, we need our users to be able to use Tiny to create, edit and update content. That tells me that this is a form - and this means we need to be able to put content in our Tiny instance, and read content from the editor too.
For ease of implementation, we are going to create a simple Vue component to act as a our Tiny editor. Rather than calling the Tiny code every time we need to use it, we can mask some of the commonly used code in our component - and even provide some helping features to make our life easier in a larger Vue app.
First things first - let’s create our component file. I’ll call mine tiny-editor.vue. In our component, we can have our template HTML code, our scripts plus any styles. We only need the template and script parts.
1<template>2 3</template>4<script>5 export default {}6</script>
Given we are building a component with two-way binding, remember that when we use v-model on the parent, it will pass a “value” prop to our component. When we’re ready, our component needs to emit an input event.
This means we need to have a prop defined for our input value, data for our internal storage of the content, and a method to emit changes back to the parent - plus our actual template code to call the Tiny Vue component.
To get us started, let’s:
Give our component a name
Import the Tiny Vue component
Set up our data
Set up our props
Create a method to emit back to the parent
1<template> 2 <editor v-model="content"></editor> 3</template> 4<script> 5 export default { 6 name: 'tiny-editor', 7 components: { 8 editor: () => import(/* webpackChunkName: "tinymce" */ "@tinymce/tinymce-vue"), 9 },10 data: function () {11 return {12 content: this.value // default to the passed value13 }14 },15 props: {16 value: {17 type: String,18 default: ''19 }20 },21 methods: {22 update: function () {23 // pass updated content back to the parent24 this.$emit('input', this.content);25 }26 }27 }28</script>
This is giving us a neat way to get our input (the “value” prop), keep track of our content (the “content” data) and emit the updates back to the parent (the “update” method).
But right now, it won’t do anything.
We also need to configure our Tiny instance, and also make sure that “update” method gets called.
Firstly, let’s import our tinyConfig helper, and we’ll create two computed properties to get our properties out.
1<template> 2 <editor 3 :api-key='getTinyKey' 4 :init="getTinyConfig" 5 v-model="content"></editor> 6</template> 7<script> 8 9import tinyConfig from "../js/tinyConfig";10 11export default {12 name: 'tiny-editor',13 components: {14 editor: () => import(/* webpackChunkName: "tinymce" */ "@tinymce/tinymce-vue"),15 },16 computed: {17 getTinyConfig: function () {18 return tinyConfig.getConfig();19 },20 getTinyKey: function () {21 return tinyConfig.getAPIKey();22 }23 },24 data: function () {25 return {26 content: this.value // default to the passed value27 }28 },29 props: {30 value: {31 type: String,32 default: ''33 }34 },35 methods: {36 update: function () {37 // pass updated content back to the parent38 this.$emit('input', this.content);39 }40 }41}42</script>
Starting to take shape now - we have updated our Tiny editor call to include our API key, as well as our Tiny configuration, both from our Tiny config helper.
Remember though, we had multiple configurations in our Tiny config module - we had “full” and “simple”, remember? At the moment, we’re only pulling our “full” config because that is the default in our Tiny config helper.
So let’s add a “type” prop to our Tiny component, and update our getTinyConfig computed property to request the config for our editor’s type.
1<template> 2 <editor 3 :api-key='getTinyKey' 4 :init="getTinyConfig" 5 v-model="content"></editor> 6</template> 7<script> 8 9import tinyConfig from "../js/tinyConfig";10 11export default {12 /* ... */13 computed: {14 getTinyConfig: function () {15 return tinyConfig.getConfig(this.type);16 }17 /* ... */18 },19 /* ... */20 props: {21 /* ... */22 type: {23 type: [Boolean, String],24 default: false25 }26 },27 /* ... */28}29</script>
Our “type” prop will default to false, and we can now pass it when we get the config for the instance.
So far we are able to instantiate our Tiny instance, and get specific configuration options. We can even pass it data using v-model, but if you run your code, you’ll be notice that we still can’t get our data - our component is never firing an “input” event.
Tiny Vue component to the rescue!
As well as a number of configuration options, Tiny has also exposed a number of events that we can listen for. And onChange is a great event to listen for changes to the editor.
You could use key press events but they may fire too frequently and create unnecessary updates within your app. onChange gives you the updates after they’re done or when the element gets focus - and has been good enough for me.
Within our component, we can listen for the Tiny Vue component’s onChange event, and pass responsibility to our “update” method. In other words, when Tiny fires onChange, we will take that as instruction to emit an input event to our parent. We can do this easily by adding the v-on definition to our <editor> template code:
1<template> 2 <editor 3 :api-key='getTinyKey' 4 :init="getTinyConfig" 5 v-model="content" 6 v-on:onChange="this.update"></editor> 7</template> 8<script> 9/* ... */10</script>
Our two-way binding is now fully functional. Our “value” prop goes in to our component, managed its value through our internal “content” data and is emitted as an “input” event when the TinyMCE editor fires its onChange event. Pretty easy, hey?
Any time we want to use an editor in our app, we can simple add it to our template code. You can either define the component globally, or within individual components. Either way, it needs to be registered somehow.
For a default editor, we can:
1<tiny-editor v-model="data"></tiny-editor>
This will create our editor with our “full” Tiny configuration, and bind its content to a data property called “data”
If we want to create a “simple” editor, we can pass a “type” property with the value “simple”.
1<tiny-editor v-model="data" type="simple"></tiny-editor>
Our component now makes it incredibly easy to re-use Tiny anywhere within our application, with two-way binding, without having to manage the Tiny Vue component code every single time we need it.
Interacting with the Tiny instance
But how do we interact with the Tiny instance? Tiny have exposed so many events and properties (you did read the documentation, right?), and we can now implement access to those that we need within our component.
Let’s get our component to...
… tell our app that it has loaded
Maybe our app has a loader that blocks access until the page is ready to load. When Tiny has loaded, it needs to tell our app that it is done so that it can turn off the loader.
We can use the onInit event from the Tiny Vue component to be told when Tiny is ready to go.
Just like our onChange event, we can add onInit to our <editor> template code, and create a method in our component that can do what we need it to do.
Which is… well, that depends on your app. Maybe your app is maintaining a count of the elements that are pending load, and when Tiny loads, this can be decreased by one. Maybe your app only has this to wait for, and when it is finished, can emit an event telling the app it is done. What you do here is up to you, but the updates to our component have the same requirement - update our template to listen for onInit, and then do something within our script when onInit is fired.
1<template> 2 <editor 3 :api-key='getTinyKey' 4 :init="getTinyConfig" 5 v-model="content" 6 v-on:onChange="this.update" 7 v-on:onInit="this.loaded"></editor> 8</template> 9<script>10export default {11 /* ... */12 methods: {13 loaded: function () {14 // tiny has loaded, now say we are loaded15 },16 /* ... */17 }18}19</script>
… be read only
There are times when you may need to disable (or enable) an editor instance based on other user input - maybe the field is conditional, maybe another action has disabled it for some reason.
This is actually really straight forward, and keeping with Vue’s reactiveness, we can simply create a new prop (let’s call it “readonly”) and pass this along.
I want my readonly prop to be a boolean value, and “false” by default - in other words, I want my editor to be editable by default.
We can then update our <editor> code to include the disabled property of the Tiny Vue component, setting our “readonly” prop:
1<template> 2 <editor 3 :api-key='getTinyKey' 4 :init="getTinyConfig" 5 :disabled="readonly" 6 v-model="content" 7 v-on:onChange="this.update" 8 v-on:onInit="this.loaded"></editor> 9</template>10<script>11export default {12 /* ... */13 props: {14 /* ... */15 readonly: {16 type: Boolean,17 required: false,18 default: false19 },20 /* ... */21 },22 /* ... */23}24</script>
Any time our component’s “readonly” prop is updated at the parent, that will update within our component and keep the Tiny instance up to date.
… get the editor instance for more advanced operations
This is a more advanced one, but may be a timesaver for you. The Tiny instance (outside of Vue or Angular or jQuery) is incredibly powerful, and has a broadly exposed API to help us do more with it.
If what you need is not exposed with Tiny’s Vue component, you may need to call the editor instance yourself.
Following on from the onInit event above - but we want to make sure we’re storing the editor instance. Every event will pass two arguments - the event, and a reference to the editor. So let’s save that last one:
1<template> 2 <editor 3 :api-key='getTinyKey' 4 :init="getTinyConfig" 5 :disabled="readonly" 6 v-model="content" 7 v-on:onChange="this.update" 8 v-on:onInit="this.loaded"></editor> 9</template>10<script>11export default {12 /* ... */13 data: function () {14 return {15 /* ... */16 editor: false17 }18 },19 /* ... */20 methods: {21 loaded: function (event, editor) {22 23 // tiny has loaded, now say we are loaded24 25 // update the editor26 this.editor = editor;27 28 // if readonly, set state29 if (this.readonly) {30 this.editor.setMode('readonly');31 }32 },33 /* ... */34 }35}36</script>
Don’t forget you’ll also need to update your data to store “editor” too.
When you need to call a method of the editor instance, you can now refer to “this.editor”, and perform your specific needs.
The finished product
We now have a simple and modular Tiny component to help us instantiate the Tiny Vue component with ease, centralise our Tiny configuration and make it really easy to extend moving forward.
Here’s our finished component:
1<template> 2 <editor 3 :api-key='getTinyKey' 4 :init="getTinyConfig" 5 :disabled="readonly" 6 v-model="content" 7 v-on:onChange="this.update" 8 v-on:onInit="this.loaded"></editor> 9</template>10<script>11 12import tinyConfig from "../js/tinyConfig";13 14export default {15 name: 'tiny-editor',16 components: {17 editor: () => import(/* webpackChunkName: "tinymce" */ "@tinymce/tinymce-vue"),18 },19 computed: {20 getTinyConfig: function () {21 return tinyConfig.getConfig(this.type);22 },23 getTinyKey: function () {24 return tinyConfig.getAPIKey();25 }26 },27 data: function () {28 return {29 content: this.value, // default to the passed value30 editor: false31 }32 },33 props: {34 value: {35 type: String,36 default: ''37 },38 readonly: {39 type: Boolean,40 required: false,41 default: false42 },43 type: {44 type: [Boolean, String],45 default: false46 }47 },48 methods: {49 loaded: function (event, editor) {50 51 // tiny has loaded, now say we are loaded52 53 // update the editor54 this.editor = editor;55 56 // if readonly, set state57 if (this.readonly) {58 this.editor.setMode('readonly');59 }60 },61 update: function () {62 // pass updated content back to the parent63 this.$emit('input', this.content);64 }65 }66}67</script>
If you want to get this up and running yourself, check out the Github repo for this article.
One more thing… including our Tiny plugins
Just a heads up, this is where it gets a bit specific using the Copy Plugin and UglifyJS for Webpack to achieve this step. Regardless of your build tool, the concepts here are the same - you may need to implement them differently for your process.
External plugins are an incredible way to extend what Tiny can do - and Tiny have exposed a superb level of UI elements to help us do this, including dialogs and an autocompleter.
But what’s the point of being able to write plugins if they’re not part of our build process?
Tiny has a very specific way that it looks for external plugins - either in the source folder for Tiny, or specified at a specific URL. Given we’re using Tiny Cloud, we need to explicitly specify where our plugins are located.
While developing our code, we want to make it readable - but for production, minified is better - the catch being that we also need to be able to tell Tiny in our config exactly where each file lives.
During the build process with Webpack, I am using UglifyJS to minify the content, and the Copy Webpack Plugin to copy the minified content to a location in my dist directory. This way I have my plugins in the src folder, and the plugins minified for production as part of the build process.
In the Tiny config module, I can now reference the minified plugins that are stored in the dist directory for me.
1let config = {2 ...3 external_plugins: {4 'helloworld': '/dist/tiny/plugins/helloworld/plugin.js'5 },6 ...7};
Example setup
Want to play with some code? Take a look at the Github repo for this article for full source code - you can tinker with code, run the build scripts yourself, and get yourself primed for your own Tiny and Vue integration.
You can also take a look at a working demo to see how the component runs.