dynamic rendering compilation of vue2 templates with components embedded - vue.js

I hope somebody can help me with my vue.js question regarding version 2.6.14
I created a component "dynamiccontent" capable of displaying html string generated from a backend api (with vue variables/functions in). the component utilizes vue.compile and render functions. it works great for the test case "slide working" in the app when I use my code for normal html tags.
here is the test function
Vue.component("test-slide", {
template:
'<div :key="slide.id"><dynamiccontent v-bind:slide="slide"></dynamiccontent></div>',
props: ["slide"]
});
var vm = new Vue({
el: "#player",
template:
"<div>working: <test-slide v-bind:slide='slideworking'></test-slide><br> problem: <test-slide v-bind:slide='slidenotworking'></test-slide></div>",
methods: {
test() {
return "test";
}
},
data: function () {
return {
slideworking: {
id: "dkd",
content:
"<div>test {{this.getDynamic()}}<div><div>test {{this.getDynamic()}}<div>test {{this.getDynamic()}}</div></div></div></div>"
},
slidenotworking: {
id: "dkd",
content:
"<div>test {{this.getDynamic()}}<test-component>{{this.getDynamic()}}<test-component>test {{this.getDynamic()}}<test-component>test {{this.getDynamic()}}</test-component></test-component></test-component></div>"
}
};
}
});
as soon es I try to use variables in cascaded components (see the sample data "slide notworking", which uses vue components as well, in this example testcomponent) the compile function seems to ignore all the Childs.
The Problem as a codepen with the demo source
codepen.io
What is expected?
vue.compile should compile the child components as well
What is actually happening?
vue compile seems to stop at the first vue component

Related

How to correctly register a CMS element with the Shopware 6 Admin SDK

With the Shopware 6 Admin SDK it's possible to add CMS Elements to the Shopware Admin from an external app via iFrame. However, by following the documentation there are some issues one might tackle along the way. If everything is done as stated in the docs following issues occur that I'd like to discuss:
First issue: If the newly registered CMS element is used more than once in a shopping experience layout the same config data is shared between all elements. This is the most severe issue that I need to solve. The second issue sounds kind of the reason for it. Therefore I attached my code at the end.
Second issue: Once the newly registered CMS element's config modal is opened the following error is thrown: The dataset id "swag-dailymotion__config-element" you tried to publish is already registered. The error is thrown in this file. I guess this has something to do in which I set up the element.
Third issue: If e.g. some text element is exchanged with the newly registered element the following console error is thrown once the element switch button is clicked and all element previews are visible: An error was captured in current module: TypeError: this.initElementConfig is not a function. Since there is a // #ts-expect-error in the responsible file I think this is not too important for the extension developer, since Shopware is already aware of it.
I have the following setup for implementing the CMS element. It differs in some way, since I am using .vue files instead of plain .ts as explained in the docs. However, except the mentioned issues, everything seems to be working fine:
This file is the entry point for the <base-app-url>
<template>
<view-renderer v-if="showViewRenderer"></view-renderer>
</template>
<script lang="ts">
import Vue from 'vue';
import { location, cms } from '#shopware-ag/admin-extension-sdk';
import viewRenderer from '#/components/cms/view-renderer.vue';
import { CONSTANTS } from '#/components/cms/index';
export default Vue.extend({
components: {
'view-renderer': viewRenderer
},
data() {
return {
showViewRenderer: false
};
},
created(): void {
if (location.isIframe()) {
if (location.is(location.MAIN_HIDDEN)) {
this.mainCommands();
} else {
this.showViewRenderer = true;
}
}
},
methods: {
mainCommands(): void {
this.registerCmsElements();
},
registerCmsElements(): void {
cms.registerCmsElement({
name: CONSTANTS.CMS_ELEMENT_NAME,
label: this.$t('cms.dailymotion.preview.label') as string,
defaultConfig: {
dailyUrl: {
source: 'static',
value: '',
},
},
});
}
}
});
</script>
#/components/cms/view-renderer.vue
<template>
<div>
<swag-dailymotion-config v-if="location.is('swag-dailymotion-config')"></swag-dailymotion-config>
<swag-dailymotion-element v-else-if="location.is('swag-dailymotion-element')"></swag-dailymotion-element>
<swag-dailymotion-preview v-else-if="location.is('swag-dailymotion-preview')"></swag-dailymotion-preview>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { location } from '#shopware-ag/admin-extension-sdk';
import swagDailymotionConfig from '#/components/cms/swag-dailymotion/swag-dailymotion-config.vue';
import swagDailymotionElement from '#/components/cms/swag-dailymotion/swag-dailymotion-element.vue';
import swagDailymotionPreview from '#/components/cms/swag-dailymotion/swag-dailymotion-preview.vue';
export default Vue.extend({
components: {
'swag-dailymotion-config': swagDailymotionConfig,
'swag-dailymotion-element': swagDailymotionElement,
'swag-dailymotion-preview': swagDailymotionPreview
},
data() {
return {
location
};
},
created(): void {
location.startAutoResizer();
}
});
</script>
The individual components are not doing anything special. They are fetching the element with this.element = await data.get({ id: CONSTANTS.PUBLISHING_KEY }); as stated in the docs and doing some work with it. However, it's always fetching the same element without considering the slot the current element is in, therefore it's always fetching the same data set.
What am I doing wrong?

Vue.js: is it possible to have a SFC factory?

I'm using single-file-components in a larger project and I'm new to Vue.JS. I'm looking for a way to dynamically create components, especially their templates on-the-fly at run-time.
My idea is to create a "component factory" also as a SFC and to parameterize it with props, e.g. one for the template, one for the data and so forth. I get how this works for already specified SFC and that I can simply exchange them with <component v-bind:is= ..., however the task here is different. I need to compose a template string, which potentially is composed of other component instance declarations, on-the-fly and inject it in another SFC. The code below doesn't work unfortunately.
<template>
<div>
<produced-component/>
</div>
</template>
<style></style>
<script>
import Vue from 'vue'
export default {
props: {
template: { type: String, default: '<div>no template prop provided</div>' }
},
components: {
'produced-component': Vue.extend(
{
template: '<div>my runtime template, this I want to be able to compose on-the-fly</div>'
}
)
}
}
</script>
It says:
The following answer: https://stackoverflow.com/a/44648296/4432432 answers the question partially but I can't figure out how to use this concept in the context of single-file-components.
Any help is greatly appreciated.
Edit 2020.03.16:
For future reference the final result of the SFC factory achieved as per the accepted answer looks like this:
<template>
<component:is="loader"/>
</template>
<script>
const compiler = require('vue-template-compiler')
import Vue from 'vue'
export default {
props: {
templateSpec: { type: String, default: '<div>no template provided</div>' }
},
computed: {
loader () {
let compSpec = {
...compiler.compileToFunctions(this.templateSpec)
}
return Vue.extend(compSpec)
}
}
}
</script>
You can surely have a Factory component with SFC; though what you are trying to do is a very rare case scenario and seems more like an anti/awkward pattern.
Whatever, you are doing currently is right. All you need to do is - use Full build of Vue which include the Vue Template Compiler, which will enable you to compile template at run-time (on the browser when app is running). But remember that for the components written in SFC, they must be compiled at build time by vue-loader or rollup equivalent plugin.
Instead of using vue.runtime.min.js or vue.runtime.js, use vue.min.js or vue.js.
Read Vue installation docs for more details.
Assuming you are using Webpack, by default main field of Vue's package.json file points to the runtime build. Thus you are getting this error. You must tell webpack to use full bundle. In the configuration resolve the Vue imports to the following file:
webpack.config.js
module.exports = {
// ... other config
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
};

Issues including Moment.js library for use with Tabulator in Vue.js

I am trying to use the Date Time functionality with Tabulator which requires the moment.js library. In my application before adding Tabulator, moment.js was already being used in certain components that level. I have a new test component that uses Tabulator and attempts to use datetime. Typically I would just import moment and use it here but it seems that moment is required within Tabulator itself.
My first thought is that Moment.js needs to be setup globally in my application so I did that.
Main.js:
```
import Vue from 'vue'
import App from './App'
import router from './router'
import { store } from './store'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import moment from 'moment'
Vue.prototype.moment = moment
...............
new Vue({
el: '#app',
data () {
return {
info: null,
loading: true,
errored: false // this.$root.$data.errored
}
},
router,
components: { App },
template: '<App/>',
store
})
```
In my component (Testpage.vue)
```
<template>
<div>
<div ref="example_table"></div>
</div>
</template>
<script>
// import moment from 'moment'
var Tabulator = require('tabulator-tables')
export default {
name: 'Test',
data: function () {
return {
tabulator: null, // variable to hold your table
tableData: [{id: 1, date: '2019-01-10'}] // data for table to display
}
},
watch: {
// update table if data changes
tableData: {
handler: function (newData) {
this.tabulator.replaceData(newData)
},
deep: true
}
},
mounted () {
// instantiate Tabulator when element is mounted
this.tabulator = new Tabulator(this.$refs.example_table, {
data: this.tableData, // link data to table
columns: [
{title: 'Date', field: 'date', formatter: 'datetime', formatterParams: {inputFormat: 'YYYY-MM-DD', outputFormat: 'DD/MM/YY', invalidPlaceholder: '(invalid date)'}}
],
}
</script>
```
I receive the error: "Uncaught (in promise) Reference Error: moment is not defined at Format.datetime (tabulator.js?ab1f:14619)"
I am able to use moment in other components by using this.$moment() but I need for it to be available in node_modules\tabulator-tables\dist\js\tabulator.js
since thats where the error is happening. Any idea how to include the library?
Go back to the first option you were trying, because annotating the Vue prototype with moment is definitely not the right approach. Even if it was recommended (which it isn't), Tabulator would have to know to find it by looking for Vue.moment. It isn't coded to do that.
One of the things I love about Open Source is that you can see exactly what a library is doing to help fix the issue. A quick search of the Tabulator code base finds this:
https://github.com/olifolkerd/tabulator/blob/3aa6f17b04cccdd36a334768635a60770aa10e38/src/js/modules/format.js
var newDatetime = moment(value, inputFormat);
The formatter is just calling moment directly, without importing it. It's clearly designed around the old-school mechanism of expecting libraries to be available globally. In browser-land that means it's on the "window" object. Two quick options could resolve this:
Use a CDN-hosted version of Moment such as https://cdnjs.com/libraries/moment.js/ by putting something like this in the header of your page template:
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.23.0/moment.min.js"></script>
Adjust your code above to set moment on window:
window.moment = moment;
ohgodwhy's comment above isn't necessarily wrong from the perspective of date-fns being better in many ways. But it won't work for you because Tabulator is hard-coded to look for moment, so you'll need moment itself to work.

How can I add vue-router link as ag-grid-vue column?

ag-grid-vue documentation from ag-grid website clearly says:
You can provide Vue Router links within the Grid, but you need to
ensure that you provide a Router to the Grid Component being created.
with sample code:
// create a new VueRouter, or make the "root" Router available
import VueRouter from "vue-router";
const router = new VueRouter();
// pass a valid Router object to the Vue grid components to be used within the grid
components: {
'ag-grid-vue': AgGridVue,
'link-component': {
router,
template: '<router-link to="/master-detail">Jump to Master/Detail</router-link>'
}
},
// You can now use Vue Router links within you Vue Components within the Grid
{
headerName: "Link Example",
cellRendererFramework: 'link-component',
width: 200
}
What's missing here is how to make the "root" Router available. I've been looking into various sources and see many people have the same problem, but none got a clear answer.
https://github.com/ag-grid/ag-grid-vue/issues/1
https://github.com/ag-grid/ag-grid-vue/issues/23
https://github.com/ag-grid/ag-grid-vue-example/issues/3
https://forum.vuejs.org/t/vue-cant-find-a-simple-inline-component-in-ag-grid-vue/21788/10
Does ag-grid-vue still work with vue-router, then how, or is this just outdated documentation? Some people claim it worked for them so I assume it worked at one point.
I am not looking for cool answer at this point. I just want to know if it is possible. I tried passing router using window or created() and none worked so far.
Thank you!
the approach suggested by #thirtydot works well. The only downside was the user cannot right-click, but I found you can just define href link. So when you left-click, event listener makes use of router. When you right-click and open in new tab, browser takes href link.
You still need to make your root router available. Below code sample assumes you have the code inside the vue-router-aware Vue component that consumes ag-grid, hence this.$router points to the root router.
{
headerName: 'ID',
field: 'id',
cellRenderer: (params) => {
const route = {
name: "route-name",
params: { id: params.value }
};
const link = document.createElement("a");
link.href = this.$router.resolve(route).href;
link.innerText = params.value;
link.addEventListener("click", e => {
e.preventDefault();
this.$router.push(route);
});
return link;
}
}

v-if behave differently: from vue project 1.x to 2.x migration

In my vue project, i am tring to upgrade from 1.x to 2.x. And I come across a problem as following:
<div id="demo">
<service :service-name="serviceName" ref="testcomp" v-if="runTest"></service>
</div>
on the child component service, I use v-if to control its existence. And in the parent component, I want to use this.$refs.testcomp to invoke the method test defined inside the child component as following:
methods: {
init() {
this.runTest = true;
},
},
mounted() {
this.init();
},
watch: {
runTest() {
if(this.runTest) {
console.log('1');
this.$refs.testcomp.test();
console.log('2');
}
},
},
But the error message said that this.$refs.testcomp is undefined. I have created the live demo with jsfiddle: https://jsfiddle.net/baoqger/0r4vby5n/1/
But in vue1.x, it can work well. https://jsfiddle.net/baoqger/fzuajwkz/1/
I know change to v-show, can skip this issue. But in my real project(it is an big project, developed by others. So if i change v-if to v-show, it will trigger other issues related to work flow). So is there any way to make the v-if still work in the upgraded version.
I think it is because when you called this.$refs.testcomp.test() the element is not ready yet. Try doing this instead:
this.$nextTick(() =>{
this.$refs.testcomp.test();
})