Chris
Chris

Reputation: 2035

Vue.js + Require.js extending parent

I'm new to Vue.js but trying to get to grips with it. So far it has gone well and I've got quite far but I am stuck extending a parents template.

I am trying to make dashboard widgets that extend a default widget layout (in Boostrap). Please note that the below code is using Vue, Require, Underscore & Axios.

Parent file - _Global.vue

<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <b>{{ widgetTitle }}</b>
            <div class="pull-right">
                <a href="#" v-on:click="toggleMinimized"
                   v-bind:title="(isMinimized ? 'Show widget' : 'Hide widget')">
                    <i class="fa fa-fw" v-bind:class="isMinimized ? 'fa-plus' : 'fa-minus'"></i>
                </a>
            </div>
        </div>
        <div class="panel-body" v-if="!isMinimized">


            <div class="text-center text-muted" v-if="!isLoaded">
                <i class="fa fa-spin fa-circle-o-notch"></i><br />
            </div>

            <parent v-if="isLoaded">
                <!-- parent content should appear here when loaded -->
            </parent>
        </div>
    </div>
</template>
<script>
    export default {

        // setup our widget props
        props: {
            'minimized': {
                'default': false,
                'required': false,
                'type': Boolean
            }
        },

        // define our data
        data: function () {
            return {
                widgetTitle: 'Set widget title in data',
                isLoaded: false,
                isMinimized: this.$props.minimized
            }
        },

        // when vue is mounted, open our widget
        mounted: function () {
            if(!this.isMinimized) {
                this.opened();
            }
        },

        // define our methods
        methods: {

            // store our widget state to database
            storeWidgetState: function () {

                // set our data to send
                let data = {
                    'action' : 'toggleWidget',
                    'widget' : this.$options._componentTag,
                    'state' : !this.isMinimized
                };

                // post our data to our endpoint
                axios.post(axios.endpoint, data);
            },

            // toggle our minimized data
            toggleMinimized: function (e) {

                // prevent default
                e.preventDefault();

                // toggle our minimized state
                this.isMinimized = !this.isMinimized;

                // trigger opened if we aren't minimized
                if(!this.isMinimized) this.opened();

                // save our widget state to database
                this.storeWidgetState();
            },

            // triggered when opened from being minimized
            opened: function () {
                console.log('opened() method is where all widget logic should be placed');
            }
        }

    }
</script>

Child file - Example.vue Should extend _Global.vue using mixins and then display content within .panel-body

<template>
    <div>
        I want this content to appear inside the .panel-body div

        {{ content }}

         <img v-bind:src="image.src" v-bind:alt="image.alt"
             v-if="image.src" class="img-responsive" style="margin: 0 auto" />
    </div>
</template>
<script>

    // import our widgets globals
    import Global from './_Global.vue'

    export default {

        components: {
            'parent': {
                // what can I possibly put here??
            }
        },

        // use our global mixin for all widgets
        mixins: [Global],

        // setup our methods for this widget
        methods: {

            opened: _.debounce(function () {

                // make sure this can only be opened once
                if(this.hasBeenOpened) return;
                this.hasBeenOpened = true;

                // temporarily allow axios to make external requests
                let axiosHeaders = axios.defaults.headers.common;
                let vm = this;
                axios.defaults.headers.common = {};

                axios.get('https://yesno.wtf/api')
                    .then(function (res) {

                        // set our content
                        vm.content = null;

                        // set our image content
                        vm.image.src = res.data.image;
                        vm.image.alt = res.data.answer;

                    })
                    .catch(function (err) {

                        // set our error text
                        vm.content = String(err);

                    })
                    .then(function () {

                        // this will always hit..
                        vm.isLoaded = true;

                    });

                // restore our axios headers for security
                axios.defaults.headers.common = axiosHeaders;
            }, 300)
        },

        // additional data
        data: function () {
            return {

                // set our widgets title
                widgetTitle: 'Test title',

                // logic for the specific widget
                hasBeenOpened: false,
                content: 'Loaded and ready to go...',
                image: {
                    src: false,
                    alt: null
                }
            };
        },

    }
</script>

Currently my parent template is just completely overwriting my child view. The only way I can get it to work is by explicitly defining the template parameter inside components -> parent: {} but I don't want to have to do that...?

Upvotes: 1

Views: 463

Answers (1)

Chris
Chris

Reputation: 2035

Ok, thanks to Gerardo Rosciano for pointing me the in right direction. I've used to slots to come up with an eventual solution. We then access parent methods and data attributes just to get everything working as it should.

Example.vue - our example widget

<template>
    <div>
        <widget-wrapper>
            <span slot="header">Example widget</span>
            <div slot="content">
                <img v-bind:src="image.src" v-bind:alt="image.alt"
                     v-if="image.src" class="img-responsive" style="margin: 0 auto" />

                {{ content }}
            </div>
        </widget-wrapper>
    </div>
</template>
<script>

    // import our widgets globals
    import WidgetWrapper from './_Widget.vue'

    export default {

        // setup our components
        components: {
            'widget-wrapper': WidgetWrapper
        },

        // set our elements props
        props: {
            'minimized': {
                'type': Boolean,
                'default': false,
                'required': false
            }
        },

        // setup our methods for this widget
        methods: {

            loadContent: _.debounce(function () {

                // make sure this can only be opened once
                if(this.hasBeenOpened) return;
                this.hasBeenOpened = true;

                // temporarily allow axios to make external requests
                let axiosHeaders = axios.defaults.headers.common;
                let vm = this;
                axios.defaults.headers.common = {};

                axios.get('https://yesno.wtf/api')
                    .then(function (res) {

                        // set our content
                        vm.content = null;

                        // set our image content
                        vm.image.src = res.data.image;
                        vm.image.alt = res.data.answer;

                    })
                    .catch(function (err) {

                        // set our error text
                        vm.content = String(err);

                    })
                    .then(function () {

                        // this will always hit..
                        vm.isLoaded = true;

                    });

                // restore our axios headers for security
                axios.defaults.headers.common = axiosHeaders;
            }, 300)
        },

        // additional data
        data: function () {
            return {

                // global param for parent
                isLoaded: false,

                // logic for the specific widget
                hasBeenOpened: false,
                content: 'Loaded and ready to go...',
                image: {
                    src: false,
                    alt: null
                }
            };
        },

    }
</script>

_Widget.vue - our base widget that gets extended

<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <b><slot name="header">Slot header title</slot></b>
            <div class="pull-right">
                <a href="#" v-on:click="toggleMinimized"
                   v-bind:title="(minimized ? 'Show widget' : 'Hide widget')">
                    <i class="fa fa-fw" v-bind:class="minimized ? 'fa-plus' : 'fa-minus'"></i>
                </a>
            </div>
        </div>
        <div class="panel-body" v-if="!minimized">
            <div class="text-center text-muted" v-if="!isLoaded">
                <i class="fa fa-spin fa-circle-o-notch"></i><br />
                Loading...
            </div>
            <div v-else>
                <slot name="content"></slot>
            </div>
        </div>
    </div>
</template>

<script>
    export default {

        // get loaded state from our parent
        computed: {
            isLoaded: function () {
                return this.$parent.isLoaded;
            }
        },

        // set our data element
        data: function () {
            return {
                minimized: false
            }
        },

        // when the widget is mounted, trigger open state
        mounted: function () {
            this.minimized = this.$parent.minimized;
            if(!this.minimized) this.opened();
        },

        // methods to manipulate our widget
        methods: {

            // save our widget state to database
            storeWidgetState: function () {

                // set our data to send
                let data = {
                    'action' : 'toggleWidget',
                    'widget' : this.$parent.$options._componentTag,
                    'state' : !this.minimized
                };

                // post this data to our endpoint
                axios.post(axios.endpoint, data);
            },

            // toggle our minimized state
            toggleMinimized: function (e) {

                // prevent default
                e.preventDefault();

                // toggle our minimized state
                this.minimized = !this.minimized;

                // trigger opened if we aren't minimized
                if(!this.minimized) this.opened();

                // save our widget state to database
                this.storeWidgetState();
            },

            // when widget is opened, load content
            opened: function () {

                // make sure we have a valid loadContent method
                if(typeof this.$parent.loadContent === "function") {
                    this.$parent.loadContent();
                } else {
                    console.log('You need to define a loadContent() method on the widget');
                }
            }
        }
    }
</script>

Upvotes: 1

Related Questions