Vue 2 + TinyMCE Single Page Confusion - vue.js

I write quite a few single page vue 2 files but have never tried using a "component" before. Can someone help spot the problem with my code? The error I'm getting is "Editor is not defined". Every example out there has you importing the vue module but I'm not using a builder so I thought just including the script(s) would work. I have removed a lot of extraneous code to make it simpler to read (I hope).
<script src="https://cdn.jsdelivr.net/npm/vue#2.X/dist/vue.js"></script>
...
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/5/tinymce.min.js" referrerpolicy="origin"></script>
<script src="https://cdn.jsdelivr.net/npm/tinymce-vue/dist/tinymce-vue.min.js"></script>
<div id="vue_app">
...
<div v-show="showtable">
...
<table class="tbdesign">
<tr>
<th>ID</th>
<th>Name</th>
...
<th>Func</th>
</tr>
<tr v-for='row in filteredRows' :key="row.property_id">
<td :id="row.property_id">{{row.property_id}}</td>
<td>{{ row.name }}</td>
...
<td><div v-on:click="editRow(row.property_id)" href="#">Edit</div>
</td>
</tr>
</table>
</div>
</section>
<section v-if="showeditor">
<div >
...
<form>
<div>
<div>
<label for="name">Name:</label> <input class="detail_update id="name" type="text" v-model="editrow.name" />
</div>
...
<div class="form-group col">
Description:<br>
<editor
apiKey="no-api-key"
v-model="editrow.description"
:init="{
height: 500,
menubar: true,
plugins: [
'advlist autolink lists link image charmap',
'searchreplace visualblocks code fullscreen',
'print preview anchor insertdatetime media',
'paste code help wordcount table'
],
toolbar:
'undo redo | formatselect | bold italic | \
alignleft aligncenter alignright | \
bullist numlist outdent indent | help'
}"
>
</editor>
</div>
<div class="form-group col">
<button v-on:click="submitData" type="button">Save</button>
</div>
</div>
</form>
</div>
...
</div>
<script type="module">
var app = new Vue({
el: '#vue_app',
data() {
return {
rows: [],
row: [],
...
editrow: [],
...
errors: []
}
},
components: {
'editor': Editor
},
mounted() {
this.init();
},
computed: {
...
},
methods: {
init() {
this.loading = true;
axios.get('/dap/api/?/functions/get_properties/')
.then(response => {
this.rows = response.data;
console.log(response.data);
this.showtable = true;
})
.catch(function(error) {
this.errored = true;
alert(error);
})
.finally(() => this.loading = false)
},
...
checkData() {
...
},
submitData() {
...
},
editRow(rowID) {
for (var i = 0; i < this.rows.length; i++) {
if (this.rows[i]['property_id'] == rowID) {
this.editrow = this.rows[i];
this.showeditor = true;
this.showtable = false;
break;
}
}
}
}
});
</script>

Editor is not actually defined anywhere in your code, and <script type="module"> uses strict mode, requiring all referenced variables to be declared upfront. Since the Editor variable doesn't exist, the script immediately fails with the error you observed. However, it doesn't look like you actually need <script type="module"> here, so you could just use a regular <script>.
Every example out there has you importing the vue module but I'm not using a builder so I thought just including the script(s) would work.
The examples that import .vue files use a build system to automatically compile the imports with vue-loader. In this case you're using a pre-compiled script from CDN, so no loader is needed, but you do need to reference the correct symbol that the tinymce-vue script defines.
The tinymce-vue script sets its exports on window.TinymceVue. The pre-built Editor.vue component happens to be exported as the same name as the root export: window.TinymceVue.TinymceVue.
So you should locally register tinymce-vue's Editor component as:
<script>
new Vue({
components: {
editor: window.TinymceVue.TinymceVue,
}
})
</script>
demo

Related

if-emoji lookup table with Vue

I'm using the npm module if-emoji to detect whether a user can view emojis or not. There is a similar tool in Modernizr.
If the user can't view emojis, I'm displaying an image of the emoji instead. So my Vue HTML looks like this:
<h2 v-if="this.emojis">😄</h2>
<h2 v-if="!this.emojis"><img src="https://example.com/emoji.png">/h2>
Does this still download the image for users who can view emojis, therefore using bandwidth unecessarily?
And is there a more efficient way of doing this, so that I don't need to go and add v-if every time I use an emoji? Can there be some sort of lookup table, if there's an emoji and !this.emojis?
You can solve this also by creating your own vue.component
<emojiorimage usesmily="false" smily="😄" imgpath="https://i.stack.imgur.com/QdqVi.png"></emojiorimage>
that capsulates the decision if image or smily inside itself.
var app = new Vue({
el: "#app",
data: function(){
return {
extendedCost: 0,
}
},
components: { "emojiorimage" : {
template : `
<h2>
usesmily is {{usesmily}}<br/>
<div v-if="usesmily === 'true'">{{ smily }}</div>
<div v-else><img :scr="imgpath" width="50" height="50" :alt="imgpath" /></div>
</h2>`,
props: ["usesmily", "smily", "imgpath"],
}}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Emoji or Image</h1>
<emojiorimage
usesmily="false"
smily="😄"
imgpath="https://i.stack.imgur.com/QdqVi.png"
></emojiorimage>
<emojiorimage
usesmily="true"
smily="😎"
imgpath="https://i.stack.imgur.com/QdqVi.png"
></emojiorimage>
</div>
You can then feed it the isemoji from your npm package that you query once and store somewhere.
For seperate vue files it would look kind of like this:
emojiorimage.vue
<template>
<h2>
<div v-if="usesmily === 'true'">{{ smily }}</div>
<div v-else><img :scr="imgpath" width="50" height="50"
:alt="imgpath" /></div>
</h2>
</template>
<script>
export default {
props: ["usesmily", "smily", "imgpath"],
};
</script>
App.vue
<template>
<div id="app">
<h1>Emoji or Image</h1>
<emojiorimage
usesmily="false"
smily="😄"
imgpath="https://i.stack.imgur.com/QdqVi.png"
/>
<emojiorimage
usesmily="true"
smily="😎"
imgpath="https://i.stack.imgur.com/QdqVi.png"
/>
</div>
</template>
<script>
import emojiorimage from "./components/emojiorimage.vue";
export default {
components: {
emojiorimage,
},
};
</script>
index.html
<div id="app"></div>
index.js
import Vue from "vue";
import App from "./App";
Vue.config.productionTip = false;
new Vue({
el: "#app",
template: "<App/>",
components: { App }
});
to get:
Learning resources:
https://v2.vuejs.org/v2/guide/components.html
https://v2.vuejs.org/v2/guide/single-file-components.html
This should work as well:
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<h2 v-if="true">😄</h2>
<h2 v-else><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/SNice.svg/1200px-SNice.svg.png" width=15 height=15></h2>
<h2 v-if="false">😄</h2>
<h2 v-else><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/SNice.svg/1200px-SNice.svg.png" width=15 height=15></h2>
The v-if part only is part of the sourcecode if the statement is true - else the else statement is in instead. If the img tag is not part of the loaded source the image wont be loaded.
<h2 v-if="emojis">😄</h2>
<h2 v-else><img src="https://example.com/emoji.png"></h2>
Is probably the only way to improve your code as #Patrick Artner pointed out.
You do not need this. in the template
To the question if the image is loaded if it is not shown the simple answer is no. Vue only renders v-ifs when they are needed and does not render it - the image is loaded only if !emojis.
Thanks to #PatrickArtner who posted the idea of using a Vue component to render the emoji / image. Here is my final solution, which resizes the emoji / image to fit the surrounding text — it uses fitty to resize the emojis, and sets the images to height:1em;.
Emoji.vue
<template>
<div class="emojiContainer">
<span id="fittyEmoji" v-if="emojis">{{ insert }}</span>
<img v-if="!emojis" :src="insert" class="emojiImage">
</div>
</template>
<style scoped>
.emojiContainer {
display: inline;
}
.emojiImage {
height:1em;
}
</style>
<script src="fitty.min.js"></script>
<script>
import fitty from 'fitty';
import ifEmoji from 'if-emoji'
export default {
name: 'Emoji',
props: ['name'],
data: function() {
return {
insert: "",
emojis: false,
}
},
methods: {
insertEmoji(){
var names = ['frog', 'fire']
var emojis = ['🐸','🔥']
var urls = ['https://example.com/frog.png',
'https://example.com/fire.png']
if (this.emojis) {
this.insert = emojis[names.indexOf(this.name)];
} else {
this.insert = urls[names.indexOf(this.name)];
}
fitty('#fittyEmoji')
}
},
beforeMount() {
if (ifEmoji('🐸')) {
this.emojis = true;
} else {
this.emojis = false;
}
this.insertEmoji();
}
}
</script>
Then in your parent components you can just insert like this:
<template>
<div id="app">
<h1><Emoji name='frog'/> Frog Facts</h1>
<p>Lorem ipsum <Emoji name='fire'/><p>
</div>
</template>
<script>
import Emoji from '#/components/Emoji.vue'
export default {
components: {
Emoji,
},
};
</script>

Strange behavior in Vue 3 without any error or warning

I have the following Vue 3 component that I am using in 2 pages that are setup like this in vue.config.js:
module.exports = {
// Put this in the ASP.NET Core directory
outputDir: "../wwwroot/app",
pages: {
vehicleDetails: "src/VehicleDetails.js",
driverDetails: "src/DriverDetails.js"
}
};
Both of these pages work and share the same underlying 'Certificates' component that looks like this:
<template>
<div>
<h3 id="Vue_Certificates">Certificates</h3>
<div>
objectId: {{objectId}} test
<table class="table table-striped table-hover table-condensed2" style="clear: both;">
<thead>
<tr>
<th><b>test</b></th>
<th style="text-align: right;">
<a href="#" #click="addCertificate">
<i class="fa fa-plus-square"></i> Add
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="certificate in certificates" v-bind:key="certificate">
<td>{{ certificate.CertificateTypeId }}</td>
<td>
<a href="#" #click="removeCertificate(index)" title="Delete" style="float: right;" class="btn btn-default">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { onMounted, ref } from "vue";
import $ from 'jquery';
import _ from 'lodash';
export default {
components: {
},
props: {
objectId: String,
ownerIsVehicle: Boolean
},
data() {
return {
certificates: [],
types: []
}
},
created() {
const requestOne = axios.get("/api/v1.0/CertificateType/GetCertificateTypes");
const requestTwo = axios.get("/api/v1.0/Certificate/GetCertificateByObjectId", { params: { objectId: this.objectId, ownerIsVehicle: this.ownerIsVehicle } });
axios.all([requestOne, requestTwo]).then(axios.spread((...responses) => {
const responseOne = responses[0];
const responseTwo = responses[1];
alert("Axios calls completed 1");
this.types = responseOne.data;
var mappedData = responseTwo.data.map(x => ({
...x,
ValidFrom: new Date(x.ValidFrom),
ValidTill: new Date(x.ValidTill)
}));
this.certificates = mappedData;
alert("Axios calls completed 2");
console.log("succes");
})).catch(errors => {
console.log("fail");
});
},
methods: {
removeCertificate(index) {
this.certificates.splice(index, 1);
},
addCertificate() {
alert("Hello?");
}
}
}
</script>
The above code does NOT render the Vue component, but once I take out the two cells containing the anchors that call addCertificate & removeCertificate it all suddenly works.
For some reason I don't get any error in Chrome dev tools .. npm console .. visual studio console .. nowhere, so I have no clue at all what is going wrong.
Important: If I take out 1 of the 2 lines in vue.config.js it works on the page that is still added. The purpose of this component is that it can be reused.
FYI: I shrank down the code as much as possible to make sure other code bits aren't the cause.
For the record, the data property "certificates" was first a ref([]) but that doesn't seem to help at all.
Thanks in advance!

Update properties of component in Vue.js

I've been searching for the answer 2nd day. But still couldn't find solution.
I have the modal window template. And the main page template from where I need to update modal window size by clicking on the button (span). Shortly it's like this for HTML:
<template id="modal">
<div>
<div :class="'modal-' + size">
...
</div>
</div>
</template>
<template id="list">
<div>
<span #click="onDetails">
Show Details
</span>
</div>
<modal size="md" #showdetails="showdetails();" ref="modal">
...
</modal>
</template>
And for JS:
Vue.component("modal", {
template: "#modal",
props: {
size: {
type: String,
default: ""
}
},
methods: {
onDetails() {
this.$emit("showdetails")
}
}
})
var List = Vue.extend({
template: "#list",
methods: {
showDetails() {
if(this.$refs.modal.size == "md") {
this.$refs.modal.size = "lg"
}
<additional code here>
}
}
})
When I'm accessing this.$refs.modal.size for read - it's OK. When I'm just changing it from showDetails - OK if only this action in the function. When I'm put something else instead of - size is not updating.
For example:
this.$refs.modal.size = "lg" - will work
this.$refs.modal.theme = "danger"; this.$refs.modal.size = "lg" - neither of them are updating
What am I doing wrong?
You need to assign the attribute value of Size by the javascript method setAttribute . Example : this.$refs.modal.setAttribute('size', 'lg')
There is a working demo below:
new Vue({
el: '#app',
methods: {
showdetails() {
console.log(this.$refs.modal.getAttribute('size'));
this.$refs.modal.setAttribute('size', 'lg')
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.15/vue.js"></script>
<div id='app'>
<button size="md" #click="showdetails" ref="modal">Click</button>
</div>

Embeded template in vue

I would like to use a local component in VueJS:
My component file (cleaned up a bit):
<template id="heroValuePair">
<td class="inner label">{{label}}</td>
<td class="inner value">{{c}}</td>
<td class="inner value">
{{t}}
<span v-if="c < t" class="more">(+{{t-c}})</span>
<span v-if="c > t" class="less">({{t-c}})</span>
</td>
</template>
<template id="hero">
<table class="hero card" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>other data...</td>
</tr>
<tr>
<hvp label="Label" v-bind:c="current.level" :t="target.level" :key="hero.id"/>
</tr>
</table>
</template>
<script>
var HeroValuePair = {
template: "#heroValuePair",
props: {
label : String,
c : Number,
t : Number
},
created() {
console.log("HVP: "+this.c+" "+this.t);
}
};
Vue.component("Hero", {
template: "#hero",
props: {
heroId : String
},
components: {
"hvp" : HeroValuePair
},
data: () => ({
hero: {},
current: {},
target: {}
}),
computed: {
},
created() {
fetch("/api/hero/"+this.heroId)
.then(res => res.json())
.then(res => {
this.hero = res.hero
this.current = res.current
this.target = res.target
})
}
});
</script>
<style>
</style>
This outer Hero template is used in a list iterator:
<template id="card-list">
<table>
Card list
<div id="">
<div v-for="card in cards" class="entry">
<Hero :hero-id="card.hero.id" :key="card.hero.id"/>
</div>
</div>
</table>
</template>
<script>
Vue.component("card-list", {
template: "#card-list",
data: () => ({
cards: [],
}),
created() {
fetch("/api/cards")
.then(res => res.json())
.then(res => {
this.cards = res.heroes
})
.catch((e) => alert("Error while fetching cards: "+e));
}
});
</script>
<style>
</style>
However, when I render the card list, it only produces the list of the first td in hvp template:
When I comment out the call of hpv the page is rendered correctly with all the HTML code from Hero template.
I tried to figure out what step I left out, but can't find the clue.
One last info: I used JavalinVue to support the server side, not nodejs-based Vue CLI. I don't know if it has any impact, but may be important.
UPDATE 1
After IVO GELOV spot the problem with multiple root tags, and because I can't move to Vue3, I tried to make it as a functional template, as he suggested. I removed the template and created the render function:
var HeroValuePair = {
template: "#heroValuePair",
functional: true,
props: {
label : String,
c : Number,
t : Number
},
render(createElement, context) {
console.log("HVP: "+context.props.c+" "+context.props.t);
if (typeof context.props.c === undefined) return createElement("td" )
else return [
createElement("td", context.props.label ),
createElement("td", context.props.c ),
createElement("td", context.props.t )
]
}
}
Although the console indicated the render is called correctly, the result is the same: there is neither the rendered nodes, nor the parent Hero component displayed. I tried to move into different file, tried the functional template format, but none worked.

Use more than one directive to add data attributes to components

I have two directives which are supposed to add data attributes to components for testing, however, only one of the directives actually gets added. The two components are Bootstrap-Vue's BFormInput and BButton.
I tried removing everything but one of the buttons and the directive is still not added i.e
<b-input-group class="sm-2 mb-2 mt-2">
<b-button
variant="primary"
#click="searchJobs"
class="rounded-0"
v-jobs-search-button-directive="{ id: 'search-button' }"
>
Search
</b-button>
</b-input-group>
wrapper.html() output is:
<b-input-group-stub tag="div" class="sm-2 mb-2 mt-2"><b-button-stub target="_self" event="click" routertag="a" variant="secondary" type="button" tag="button" class="rounded-0">
Search
</b-button-stub></b-input-group-stub>
However, it is added when instead of a button I leave in place the input form i.e.
<b-input-group class="sm-2 mb-2 mt-2">
<b-form-input
v-jobs-search-input-directive="{ id: 'input-keyword' }"
class="mr-2 rounded-0"
placeholder="Enter Search term..."
:value="this.searchConfig.Keyword"
#input="this.updateJobsSearchConfig"
/>
</b-input-group>
wrapper.html() output is:
<b-input-group-stub tag="div" class="sm-2 mb-2 mt-2"><b-form-input-stub value="" placeholder="Enter Search term..." type="text" class="mr-2 rounded-0" data-jobs-search-input-id="input-keyword"></b-form-input>
This is how I add the directives
<template>
<b-input-group class="sm-2 mb-2 mt-2">
<b-form-input
v-jobs-search-input-directive="{ id: 'input-keyword' }"
class="mr-2 rounded-0"
placeholder="Enter Search term..."
:value="this.searchConfig.Keyword"
#input="this.updateJobsSearchConfig"
/>
<b-button
variant="primary"
#click="searchJobs"
class="rounded-0"
v-jobs-search-button-directive="{ id: 'search-button' }"
>
Search
</b-button>
</b-input-group>
</template>
<script>
import { mapActions, mapState } from 'vuex'
import JobService from '#/api-services/job.service'
import JobsSearchInputDirective from '#/directives/components/jobs/JobsSearchInputDirective'
import JobsSearchButtonDirective from '#/directives/components/jobs/JobsSearchButtonDirective'
export default {
name: 'jobs-search',
directives: { JobsSearchInputDirective, JobsSearchButtonDirective },
data () {
return {
jobs: [],
pages: 0
}
},
computed: {
...mapState({
pagedConfig: state => state.jobs.paged,
searchConfig: state => state.jobs.search
})
},
methods: {
// Methods go here
}
}
jobs-search-input-directive is
export default (el, binding) => {
if (process.env.NODE_ENV === 'test') {
Object.keys(binding.value).forEach(value => {
el.setAttribute(`data-jobs-search-input-${value}`, binding.value[value])
})
}
}
jobs-search-button-directive is
export default (el, binding) => {
if (process.env.NODE_ENV === 'test') {
Object.keys(binding.value).forEach(value => {
el.setAttribute(`data-jobs-search-button-${value}`, binding.value[value])
})
}
}
This is the test I run, mounting with shallowMount
it('should call jobsSearch method on search button click event', () => {
wrapper.find('[data-jobs-search-button-id="search-button"]').trigger('click')
expect(searchJobs).toHaveBeenCalled()
})
which comes back with
Error: [vue-test-utils]: find did not return [data-jobs-search-button-id="search-button"], cannot call trigger() on empty Wrapper
However wrapper.find('[data-jobs-search-input-id="input-keyword"]') DOES find the input-form
The two directives are registered in the JobsSearch.vue component and they definitely get rendered if I remove the process.env part
I expect the attribute to be added to both components but it only gets added to the BFormInput when testing. Any help will be greatly appreciated.
I believe that the problem occurs when...
... trying to use a directive...
... on a functional child component...
... with shallowMount.
b-button is a functional component.
I've put together the demo below to illustrate the problem. It mounts the same component in 3 different ways and it only fails in the specific case outlined above.
MyComponent = {
template: `
<div>
<my-normal v-my-directive></my-normal>
<my-functional v-my-directive></my-functional>
</div>
`,
components: {
MyNormal: {
render: h => h('span', 'Normal')
},
MyFunctional: {
functional: true,
render: (h, context) => h('span', context.data, 'Functional')
}
},
directives: {
myDirective (el) {
el.setAttribute('name', 'Lisa')
}
}
}
const v = new Vue({
el: '#app',
components: {
MyComponent
}
})
document.getElementById('markup1').innerText = v.$el.innerHTML
const cmp1 = VueTestUtils.mount(MyComponent)
document.getElementById('markup2').innerText = cmp1.html()
const cmp2 = VueTestUtils.shallowMount(MyComponent)
document.getElementById('markup3').innerText = cmp2.html()
#markup1, #markup2, #markup3 {
border: 1px solid #777;
margin: 10px;
padding: 10px;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<script src="https://unpkg.com/vue-template-compiler#2.6.10/browser.js"></script>
<script src="https://unpkg.com/#vue/test-utils#1.0.0-beta.29/dist/vue-test-utils.iife.js"></script>
<div id="app">
<my-component></my-component>
</div>
<div id="markup1"></div>
<div id="markup2"></div>
<div id="markup3"></div>
I haven't really looked at the code for vue-test-utils before but stepping through in the debugger makes me suspicious of this line:
https://github.com/vuejs/vue-test-utils/blob/9dc90a3fd4818ff70e270568a2294b1d8aa2c3af/packages/create-instance/create-component-stubs.js#L99
This is the render function for the stubbed child component. It would appear that context.data.directives does contain the correct directive but they aren't being passed on in the call to h.
Contrast that with the render function in my example component MyFunctional, which passes on all of data. That's required for directives to work with a functional component but when MyFunctional gets replaced with a stub the new render function seems to drop the directives property.
The only workaround I've been able to come up with is to provide your own stub:
VueTestUtils.shallowMount(MyComponent, {
stubs: {
BButton: { render: h => h('div')}
}
})
By using a non-functional stub the directive works fine. Not sure how much value this would take away from the test though.