Casey Crookston
Casey Crookston

Reputation: 13945

Change a property's value in one component from within another component

I'm trying to wrap my head around hoe Vue.js works, reading lots of documents and tutorials and taking some pluralsight classes. I have a very basic website UI up and running. Here's the App.vue (which I'm using kinda as a master page).

(To make reading this easier and faster, look for this comment: This is the part you should pay attention to)...

<template>
    <div id="app">
        <div>
            <div>
                <CommandBar />
            </div>
            <div>
                <Navigation />
            </div>
        </div>
        <div id="lowerContent">

            <!-- This is the part you should pay attention to -->

            <template v-if="showLeftContent">
                <div id="leftPane">
                    <div id="leftContent">
                        <router-view name="LeftSideBar"></router-view>
                    </div>
                </div>
            </template>

            <!-- // This is the part you should pay attention to -->

            <div id="mainPane">
                <div id="mainContent">
                    <router-view name="MainContent"></router-view>
                </div>
            </div>
        </div>
    </div>
</template>

And then in the same App.vue file, here's the script portion

<script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';

    import CommandBar from './components/CommandBar.vue';
    import Navigation from './components/Navigation.vue';


    @Component({
        components: {
            CommandBar,
            Navigation,
        }
    })
    export default class App extends Vue {
        data() {
            return {
                showLeftContent: true // <--- This is the part you should pay attention to
            }
        }
    }
</script>

Ok, so the idea is, one some pages I want to show a left sidebar, but on other pages I don't. That's why that div is wrapped in <template v-if="showLeftContent">.

Then with the named <router-view>'s I can control which components get loaded into them in the `router\index.ts\ file. The routes look like this:

    {
        path: '/home',
        name: 'Home',
        components: {
            default: Home,
            MainContent: Home,  // load the Home compliment the main content
            LeftSideBar: UserSearch  // load the UserSearch component in the left side bar area
        }
    },

So far so good! But here's the kicker. Some pages won't have a left side bar, and on those pages, I want to change showLeftContent from true to false. That's the part I can't figure out.

Let's say we have a "Notes" component that looks like this.

<template>
    <div class="notes">
        Notes
    </div>
</template>

<script lang="ts">
    import { Component, Prop, Vue } from 'vue-property-decorator';

    @Component
    export default class Notes extends Vue {
        data() {
            return {
                showLeftContent: false  // DOES NOT WORK
            }
        }
    }
</script>

Obviously, I'm not handling showLeftContent properly here. It would seem as if the properties in data are scoped only to that component, which I understand. I'm just not finding anything on how I can set a data property in the App component and then change it in a child component when that child is loaded through a router-view.

Thanks!

EDIT:

I changed the script section of the Notes component from:

<script lang="ts">
    import { Component, Prop, Vue } from 'vue-property-decorator';

    @Component
    export default class Notes extends Vue {
        data() {
            return {
                showLeftContent: false  // DOES NOT WORK
            }
        }
    }
</script>

to:

<script lang="ts">
    import { Component, Prop, Vue } from 'vue-property-decorator';

    @Component
    export default class Notes extends Vue {
        mounted() {
            this.$root.$data.showLeftContent = false;
        }
    }
</script>

And while that didn't cause any compile or runtime errors, it also didn't have the desired effect. On Notes, the left side bar still shows.

EDIT 2:

If I put an alert in the script section of the Notes component:

export default class Notes extends Vue {
    mounted() {
        alert(this.$root.$data.showLeftContent);
        //this.$root.$data.showLeftContent = false;
    }
}

The alert does not pop until I click on "Notes" in the navigation. But, the value is "undefined".

EDIT 3:

Struggling with the syntax here (keep in mind this is TypeScript, which I don't know very well!!)

enter image description here

Edit 4:

Inching along!

export default class App extends Vue {
    data() {
        return {
            showLeftContent: true
        }
    }

    leftContent(value: boolean) {
        alert('clicked');
        this.$root.$emit('left-content', value);
    }
}

This does not result in any errors, but it also doesn't work. The event never gets fired. I'm going to try putting it in the Navigation component and see if that works.

Upvotes: 0

Views: 1770

Answers (3)

Sphinx
Sphinx

Reputation: 10729

If you'd like to access the data property (or props, options etc) of the root instance, you can use this.$root.$data. (Check Vue Guide: Handling Edge)

For your codes, you can change this.$root.$data.showLeftContent to true/false in the hook=mounted of other Components, then when Vue creates instances for those components, it will show/hide the left side panel relevantly.

Below is one demo:

Vue.config.productionTip = false
Vue.component('child', {
  template: `<div :style="{'background-color':color}" style="padding: 10px">
                Reach to root: <button @click="changeRootData()">Click me!</button>
                <hr>
                <slot></slot>
             </div>`,
  props: ['color'],
  methods: {
    changeRootData() {
      this.$root.$data.testValue += ' :) '
    }
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      testValue: 'Puss In Boots'
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
  <h2>{{testValue}}</h2>
  <child color="red"><child color="gray"><child color="green"></child></child></child>

</div>

Upvotes: 1

Sovalina
Sovalina

Reputation: 5609

As it says on @lukebearden answer you can use the emit event to pass true/false to the main App component on router-link click.

Assuming your Navigation component looks like below, you can do something like that:

#Navigation.vue
<template>
  <div>
      <router-link to="/home" @click.native="leftContent(true)">Home</router-link> - 
      <router-link to="/notes" @click.native="leftContent(false)">Notes</router-link>
  </div>
</template>

<script>
export default {
  methods: {
    leftContent(value) {
      this.$emit('left-content', value)
    }
  }
}
</script>

And in your main App you listen the emit on Navigation:

<template>
  <div id="app">
    <div>
      <Navigation @left-content="leftContent" />
    </div>
    <div id="lowerContent">
      <template v-if="showLeftContent">
         //...
      </template>

      <div id="mainPane">
         //...
      </div>
    </div>
  </div>
</template>

<script>
  //...
  data() {
    return {
      showLeftContent: true
    }
  },
  methods: {
    leftContent(value) {
      this.showLeftContent = value
    }
  }
};
</script>

Upvotes: 4

lukebearden
lukebearden

Reputation: 53

A basic approach in a parent-child component relationship is to emit events from the child and then listen and handle that event in the parent component.

However, I'm not sure that approach works when working with the router-view. This person solved it by watching the $route attribute for changes. https://forum.vuejs.org/t/emitting-events-from-vue-router/10136/6

You might also want to look into creating a simple event bus using a vue instance, or using vuex.

Upvotes: 2

Related Questions