Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom manager field with external vue component: Failed to mount component: template or render function not defined. #1279

Closed
CSorel-Catalyte opened this issue Jul 21, 2020 · 12 comments
Labels

Comments

@CSorel-Catalyte
Copy link

We are creating a custom field to be rendered in the manager as a double select box using this component: https://github.com/juliorosseti/vue-select-sides

The field class is defined as:

    [FieldType(Name = "Multiselect Custom Field", Component = "custom-multiselect")]
    public class MultiselectCustomField<T> : IField where T : IOptionSource, new()

The region is implemented as:

    public class JASearchFields
    {
        [Field(Placeholder = "Comma Separated", Title = "JA Related Competencies")]
        public MultiselectCustomField<CompetencyOptions> RelatedCompetencies { get; set; }

The region is included in the page model as:

        [Region(Display = RegionDisplayMode.Setting)]
        public Regions.JASearchFields SearchFields { get; set; }

In Piranha.Manager, the select sides component is included in the gulp file as:

        name: "piranha-deps.js",
        items: [
           ...,
            "node_modules/vue-select-sides/dist/vueSelectSides.umd.js"
        ]

The custom-select component vue file is:

<template>
    <vue-select-sides type="mirror" v-model="selected" :list="list"></vue-select-sides>
</template>
<script>
//Hard coding values to get it running
    export default {
        props: ["uid", "toolbar", "model", "meta"],
        components: {
            vueSelectSides
        },
        data() {
            return {
                selected: [],
                list: [
                    {
                        value: "afghanistan",
                        label: "Afghanistan"
                    },
                    {
                        value: "brazil",
                        label: "Brazil"
                    },
                    {
                        value: "fiji",
                        label: "Fiji"
                    },
                    {
                        value: "ghana",
                        label: "Ghana"
                    }

                ]
            }
        }
    }
</script>

The component is included in the gulp compile as:

{
        name: "piranha.contentedit.js",
        items: [
            ...,
            "assets/src/js/components/fields/custom-multiselect.vue"
        ]
},

Everything builds and compiles correctly, but when I run manager and go to a page the field label is shown on the settings modal, but the component is not.
Looking in the console, I see:

piranha-deps-dev.js:16016 [Vue warn]: Failed to mount component: template or render function not defined.

found in

---> <VueSelectSides>
       <CustomMultiselect>
         <Region>
           <Root>

I have confirmed that the select sides component is included in the piranha-deps.js (and piranha-deps-dev.js)
The compiled code in piranha.contentedit.min.js is:

Vue.component("custom-multiselect", {
    props: ["uid", "toolbar", "model", "meta"],
    components: {
        vueSelectSides: vueSelectSides
    },
    data: ()=>({
        selected: [],
        list: [{
            value: "afghanistan",
            label: "Afghanistan"
        }, {
            value: "brazil",
            label: "Brazil"
        }, {
            value: "fiji",
            label: "Fiji"
        }, {
            value: "ghana",
            label: "Ghana"
        }]
    }),
    template: '\n<vue-select-sides type="mirror" v-model="selected" :list="list"></vue-select-sides>\n'
}

Am I missing a step?

@tidyui
Copy link
Member

tidyui commented Jul 22, 2020

Hi there @CSorel-Catalyte. First I have to ask about your resource setup. You say you are adding things into piranha-deps.js and piranha.contentedit.js which probably means that you are working on a modified fork of the manager. As we have little control over the source code in forks we only give support on projects based on our NuGet packages.

The correct way to add custom javascript in projects is to add resources into the manager using this approach:

https://piranhacms.org/docs/manager-extensions/resources

Best regards

@CSorel-Catalyte
Copy link
Author

Thank you! Loading the external component as a resource doesn't change the result. The issue still exists. To be clear, we are working on a modified fork of the manager, but the modifications are limited to adding additional field types. We have followed the model that the current source uses (see date-field.vue as an example of one of your components using an external vue component.)

We have added one field type for select boxes populated from an external source. That component template used html elements and does not have any child vue components. It works perfectly following the model your core field types use.

In the case of this field type, we want to leverage an existing open source vue component to support our UX intentions. I have tried multiple vue components with the same result, so it seems to me that your current implementation of the manager doesn't support including child vue components sourced from npm, but I want to make sure that I have approached this correctly.

I understand that you can't be responsible for branches outside of your control and I'm not expecting you to spend a lot of time supporting this but I am hoping that you provide the minimal effort to support us of looking at the code snippets above and offering your expert opinion as to whether we have approached it correctly. It also strikes me that it's worth you investigating to see if there is an issue, and if so you may want to add to your backlog as we will not be the only ones with this use case.

@tidyui
Copy link
Member

tidyui commented Jul 22, 2020

I saw I misread your initial comment so I deleted my comment 😉

I’m not sure how the vue compilation works with locally included components as everything in the manager relies on globally registered components. The compilation of .vue files is handled in gulpfile.js and was submitted as a contribution, so I haven’t looked at it detail to see exactly what it handles and what it doesn’t.

Regarding branching the manager, everything you’re doing can be done without branching it which will enable you to get access to new functionality easier.

Regards

@tidyui
Copy link
Member

tidyui commented Jul 22, 2020

And btw, version 8.4 will include a SelectField which can be populated from an external data source! #1186

@tidyui tidyui closed this as completed Oct 18, 2020
@horacioj
Copy link

HI @CSorel-Catalyte . Were you able to find a solution or workaround for this problem? I'm also trying to use vue components sourced from npm, with no luck. To make things even more complex, I'm new to vue :(
Thanks!

@tidyui
Copy link
Member

tidyui commented Nov 1, 2020

@filipjansson I know the DateTimeField you put together uses an external Vue.js component that was downloaded from npm. Could you share some info on how the setup for this component is done.

@horacioj As a starter, the manager is not a Vue.js application, it is a Razor Pages web application which uses Vue.js locally on each page to bind and handle the interactive parts of the UI. This is also why we compile all of our .vue files into .js files according to the following syntax (https://vuejs.org/v2/guide/components.html)

Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

Components defined this way are registered globally in Vue and can be referenced anywhere. For example, the file text-field.vue which looks like this:

<template>
    <textarea class="form-control" rows="4" :placeholder="meta.placeholder" v-model="model.value" v-on:change="update()"></textarea>
</template>

<script>
export default {
    props: ["uid", "model", "meta"],
    methods: {
        update: function () {
            // Tell parent that title has been updated
            if (this.meta.notifyChange) {
                var title = this.model.value;
                if (title.length > 40) {
                    title = title.substring(0, 40) + "...";
                }
                this.$emit('update-title', {
                    uid: this.uid,
                    title: title
                });
            }
        }
    }
}
</script>

Will be compiled into this by our build pipeline:

Vue.component("text-field", {
  props: ["uid", "model", "meta"],
  methods: {
    update: function () {
      // Tell parent that title has been updated
      if (this.meta.notifyChange) {
        var title = this.model.value;

        if (title.length > 40) {
          title = title.substring(0, 40) + "...";
        }

        this.$emit('update-title', {
          uid: this.uid,
          title: title
        });
      }
    }
  },
  template: "\n<textarea class=\"form-control\" rows=\"4\" :placeholder=\"meta.placeholder\" v-model=\"model.value\" v-on:change=\"update()\"></textarea>\n"
});

All of this happens in our gulpfile.js.

Best regards

@horacioj
Copy link

horacioj commented Nov 2, 2020

Thank you, @tidyui. Yes, I was able to create a custom "hello world" vue component for a "hello world" block, a custom gulpfile,js to compile it, etc. What I wasn't able to do is to reuse external vue components.

Is there a repo I can see where something like this (integrate external vue components) was implemented? E,g,. is there a repo I can access where the DateTimeField you mention was implemented?

Many thanks!

@tidyui
Copy link
Member

tidyui commented Nov 2, 2020

The vue component for the date field can be found here in the project Piranha.Manager. https://github.com/PiranhaCMS/piranha.core/blob/master/core/Piranha.Manager/assets/src/js/components/fields/date-field.vue

@CSorel-Catalyte
Copy link
Author

We have not been able to use any 3rd party (i.e. npm) vue components. The only thing we have been successful with is writing our own components from scratch using an existing component as a template.

@horacioj
Copy link

horacioj commented Nov 2, 2020

It looks like it depends on how the 3rd party component was built. In case of the DateTimeField, it works without any special workaround, because it was already built as a global component, I guess? Simply referencing "node_modules/vuejs-datepicker/dist/vuejs-datepicker.min.js" works fine.

@filipmatsman
Copy link
Contributor

filipmatsman commented Nov 3, 2020

@horacioj Yes, I think you are right, I have come to the same conclusion when I have been looking at 3rd party components.
Many components are published with some sort of module wrapper. Even vuejs-datepicker looks like it is using it by looking at the source and build script.

I believe you could use many components that are built for the browser to work, it´s just a matter of how to add/activate/import it.

@horacioj
Copy link

horacioj commented Dec 7, 2020

I found a way to use any 3rd party prebuilt Vue component as global. As a plus the compilation of JS and SCSS resources is very simple, and it doesn't need any gulp. Just a few lines in a webpack.mix.js file.

Ref.: https://laravel-mix.com/ : "Laravel Mix: An elegant wrapper around Webpack for the 80% use case."

In my module project (where customs blocks, fields, etc. are implemented):

$ npm install laravel-mix --save-dev
$ copy node_modules\laravel-mix\setup\webpack.mix.js

package.json

{
  "private": true,
  "comments": {
    "about the scripts": "to run the scripts (e.g. 'npm run dev') in Windows, install this before: npm install -g win-node-env",
    "about vue and vue compiler": [
      "mix add the dev dependencies automatically. In fact, the only dev dependency you need to add is 'laravel-mix'",
      "However, given Vue will be referenced as an external resource,",
      "I think it is better to reference them here explicitly matching the version referenced by Piranha core"
    ]
  },
  "scripts": {
    "dev": "NODE_ENV=development webpack --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "prod": "NODE_ENV=production webpack --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
  },
  "devDependencies": {
    "laravel-mix": "^5.0.9",
    "resolve-url-loader": "^3.1.2",
    "sass": "^1.29.0",
    "sass-loader": "^8.0.2",
    "vue-template-compiler": "^2.6.10"
  }
}

webpack.mix.js

// See https://laravel-mix.com/
let mix = require('laravel-mix');

// OPTIONAL Disable mix-manifest.json generation (https://github.com/JeffreyWay/laravel-mix/issues/580)
Mix.manifest.refresh = _ => void 0;

// OPTIONAL Disable OS notifications (https://laravel-mix.com/docs/5.0/os-notifications)
mix.disableNotifications();

// Exclude Vue from the output bundle (https://webpack.js.org/configuration/externals/)
// NOTE: There is no need to reference Vue in package.json because mix adds it automatically.
//       However, it might be better to reference Vue explicitly (in package.json) to ensure we 
//       are using the same version that Piranha uses.
mix.webpackConfig({
    externals: {
        'vue': 'Vue'
    }
});

// Bundle JS and CSS assets
mix.js('assets/src/js/mymodule.js', 'assets/dist/js')
    .sass('assets/src/scss/mymodule.scss', 'assets/dist/css')
    .sourceMaps();

//NOTE: About the scripts in package.json:
// npm run dev => build for develpoment
// npm run prod => build for production
// IMPORTANT: in Windows, for the scripts to run please install first 'win-node-env' globally: 
//            npm install -g win-node-env

mymodule.js

// ===================================================================
// Register 3rd party components as global
// ===================================================================

// Example 3rd party component being made available as global 
// https://abhimediratta.github.io/vue-multiselect-listbox/
import vMultiselectListbox from 'vue-multiselect-listbox'
Vue.component('v-multiselect-listbox', vMultiselectListbox)

// ===================================================================
// Register custom components as global
// ===================================================================
import vueMultiSelectField from './components/data-multiselect-field';
Vue.component('data-multiselect-field', vueMultiSelectField);

import vueMultiSelectBlock from './components/data-multiselect-block';
Vue.component('data-multiselect-block', vueMultiSelectBlock);

That's it. Simple, clean and easy.

HTH,
Horacio

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants